Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions docs/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ OpenCode owns user message IDs. Do not pass Paseo-generated IDs to OpenCode prom

Every provider adapter owns its canonical user-message timeline rows. When a foreground prompt is accepted, the adapter must emit exactly one `user_message` timeline item for that submitted prompt, using the same message ID it gives to or receives from the provider runtime. Optimistic client messages are UI-only and provider transcript echoes are optional; neither is allowed to be the only source of truth. If the provider later echoes the same submitted user message, dedupe by provider-visible message ID, not by text.

Draft metadata lookups should avoid creating provider sessions when the upstream provider has top-level APIs for that metadata. Prefer `AgentClient.listModels`, `listModes`, `listCommands`, or `listFeatures` over creating a scratch `AgentSession`; scratch sessions can show up as empty native sessions in provider import/history UIs.
Draft metadata lookups should avoid creating provider sessions when the upstream provider has top-level APIs for that metadata. Prefer `AgentClient.fetchCatalog`, `listCommands`, or `listFeatures` over creating a scratch `AgentSession`; scratch sessions can show up as empty native sessions in provider import/history UIs. `fetchCatalog` is the single discovery API for models and modes — provider implementations may use one process, separate upstream calls, or static data internally, but callers outside the provider do not get separate runtime model/mode probes.

Provider session import has its own contract. The picker calls `listImportableSessions` and receives rows only: provider handle, cwd, title, prompt previews, and last activity. Import calls `importSession({ providerHandleId, cwd })` for the selected row and must not call listing again. The provider returns the resumed session, storage config, persistence handle, and hydrated timeline for that one native session; `AgentManager.importProviderSession` seeds the daemon timeline and publishes the Paseo agent only after it is ready.

Expand Down Expand Up @@ -336,14 +336,13 @@ interface AgentClient {
overrides?: Partial<AgentSessionConfig>,
launchContext?: AgentLaunchContext,
): Promise<AgentSession>;
listModels(options: ListModelsOptions): Promise<AgentModelDefinition[]>;
fetchCatalog(options: FetchCatalogOptions): Promise<ProviderCatalog>;
isAvailable(): Promise<boolean>;
// Optional:
listModes?(options: ListModesOptions): Promise<AgentMode[]>;
listImportableSessions?(
listImportableSessions(
options?: ListImportableSessionsOptions,
): Promise<ImportableProviderSession[]>;
importSession?(
importSession(
input: ImportProviderSessionInput,
context: ImportProviderSessionContext,
): Promise<ImportedProviderSession>;
Expand Down
19 changes: 13 additions & 6 deletions packages/app/src/components/provider-diagnostic-sheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -600,13 +600,20 @@ export function ProviderDiagnosticSheet({
const modelsRefreshing = isRefreshing || providerSnapshotRefreshing;

const stableDiscoveredRef = useRef<AgentModelDefinition[]>([]);
if (providerEntry?.models && providerEntry.models.length > 0) {
stableDiscoveredRef.current = providerEntry.models;
const currentModels = providerEntry?.models;
if (currentModels && currentModels.length > 0) {
stableDiscoveredRef.current = currentModels;
}
const discoveredModels =
providerEntry?.models && providerEntry.models.length > 0
? providerEntry.models
: stableDiscoveredRef.current;

const discoveredModels = useMemo(() => {
if (currentModels && currentModels.length > 0) {
return currentModels;
}
if (providerSnapshotRefreshing) {
return stableDiscoveredRef.current;
}
return [];
}, [currentModels, providerSnapshotRefreshing]);

const [clockTick, setClockTick] = useState(0);
useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import type {
AgentCapabilityFlags,
AgentClient,
AgentLaunchContext,
AgentModelDefinition,
AgentPersistenceHandle,
AgentPromptInput,
AgentProvider,
Expand All @@ -21,6 +20,7 @@ import type {
AgentSessionConfig,
AgentStreamEvent,
AgentTimelineItem,
ProviderCatalog,
} from "./agent-sdk-types.js";

/**
Expand Down Expand Up @@ -206,15 +206,18 @@ class TestAgentClient implements AgentClient {
return this.createSession(resolvedConfig);
}

async listModels(): Promise<AgentModelDefinition[]> {
return [
{
provider: this.provider,
id: "test-model",
label: "Test Model",
isDefault: true,
},
];
async fetchCatalog(): Promise<ProviderCatalog> {
return {
models: [
{
provider: this.provider,
id: "test-model",
label: "Test Model",
isDefault: true,
},
],
modes: [],
};
}

async isAvailable(): Promise<boolean> {
Expand Down
73 changes: 41 additions & 32 deletions packages/server/src/server/agent/agent-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,25 +99,28 @@ class TestAgentClient implements AgentClient {
return new TestAgentSession(config);
}

async listModels() {
return [
{
provider: "codex",
id: "gpt-5.4",
label: "GPT-5.4",
isDefault: true,
},
{
provider: "codex",
id: "gpt-5.4-mini",
label: "GPT-5.4 Mini",
},
{
provider: "codex",
id: "gpt-5.2-codex",
label: "GPT-5.2 Codex",
},
];
async fetchCatalog() {
return {
models: [
{
provider: "codex",
id: "gpt-5.4",
label: "GPT-5.4",
isDefault: true,
},
{
provider: "codex",
id: "gpt-5.4-mini",
label: "GPT-5.4 Mini",
},
{
provider: "codex",
id: "gpt-5.2-codex",
label: "GPT-5.2 Codex",
},
],
modes: [],
};
}

async resumeSession(
Expand Down Expand Up @@ -667,8 +670,11 @@ test("setAgentMode persists the selected mode across session reload", async () =
});
}

async listModels() {
return [{ provider: "codex", id: "gpt-5.4", label: "GPT-5.4", isDefault: true }];
async fetchCatalog() {
return {
models: [{ provider: "codex", id: "gpt-5.4", label: "GPT-5.4", isDefault: true }],
modes: [],
};
}
}

Expand Down Expand Up @@ -1454,15 +1460,18 @@ test("resumeAgentFromPersistence keeps metadata config, applies overrides, and p
return new TestAgentSession(config);
}

async listModels() {
return [
{
provider: "codex",
id: "gpt-5.4",
label: "GPT-5.4",
isDefault: true,
},
];
async fetchCatalog() {
return {
models: [
{
provider: "codex",
id: "gpt-5.4",
label: "GPT-5.4",
isDefault: true,
},
],
modes: [],
};
}

async resumeSession(
Expand Down Expand Up @@ -5976,8 +5985,8 @@ class RecordingPersistedAgentsClient implements AgentClient {
throw new Error(`unexpected resumeSession for ${this.provider}`);
}

async listModels() {
return [];
async fetchCatalog() {
return { models: [], modes: [] };
}

async listImportableSessions() {
Expand Down
4 changes: 2 additions & 2 deletions packages/server/src/server/agent/agent-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3644,8 +3644,8 @@ export class AgentManager {
const client = this.clients.get(normalized.provider);
if (client) {
try {
const models = await client.listModels({ cwd: normalized.cwd, force: false });
const defaultModel = models.find((model) => model.isDefault) ?? models[0];
const catalog = await client.fetchCatalog({ cwd: normalized.cwd, force: false });
const defaultModel = catalog.models.find((model) => model.isDefault) ?? catalog.models[0];
if (defaultModel) {
normalized.model = defaultModel.id;
}
Expand Down
17 changes: 11 additions & 6 deletions packages/server/src/server/agent/agent-sdk-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -636,14 +636,14 @@ export interface AgentSession {
} | null;
}

export interface ListModelsOptions {
export interface FetchCatalogOptions {
cwd: string;
force: boolean;
}

export interface ListModesOptions {
cwd: string;
force: boolean;
export interface ProviderCatalog {
models: AgentModelDefinition[];
modes: AgentMode[];
}

export interface AgentClient {
Expand All @@ -659,8 +659,13 @@ export interface AgentClient {
overrides?: Partial<AgentSessionConfig>,
launchContext?: AgentLaunchContext,
): Promise<AgentSession>;
listModels(options: ListModelsOptions): Promise<AgentModelDefinition[]>;
listModes?(options: ListModesOptions): Promise<AgentMode[]>;
/**
* Discover models and modes together. Implementations may use one upstream
* process, separate upstream calls, static modes, or private helpers; callers
* outside the provider do not get separate runtime model/mode probes.
* The registry is responsible for merging configured model overrides.
*/
fetchCatalog(options: FetchCatalogOptions): Promise<ProviderCatalog>;
resolveCreateConfig?(input: ResolveAgentCreateConfigInput): ResolveAgentCreateConfigResult;
isCreateConfigUnattended?(input: AgentCreateConfigUnattendedInput): boolean;
listCommands?(config: AgentSessionConfig): Promise<AgentSlashCommand[]>;
Expand Down
5 changes: 1 addition & 4 deletions packages/server/src/server/agent/mcp-parity.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,12 +163,9 @@ function createRecordingAgentClients(): Record<AgentProvider, AgentClient> {
},
resumeSession: async (handle, overrides, launchContext) =>
await client.resumeSession(handle, overrides, launchContext),
listModels: async (options) => await client.listModels(options),
fetchCatalog: async (options) => await client.fetchCatalog(options),
isAvailable: async () => await client.isAvailable(),
};
if (client.listModes) {
wrappedClient.listModes = async (options) => await client.listModes!(options);
}
if (client.resolveCreateConfig) {
wrappedClient.resolveCreateConfig = (input) => client.resolveCreateConfig!(input);
}
Expand Down
Loading
Loading