From ed4f833813b6b88917be6611c468593bbcfb86fd Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Tue, 7 Jul 2026 21:45:58 -0500 Subject: [PATCH] fix(core): preserve provider metadata namespaces (#35817) Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Co-authored-by: Aiden Cline --- packages/core/src/aisdk.ts | 5 +- packages/core/src/session/runner/llm.ts | 5 +- .../src/session/runner/publish-llm-event.ts | 12 ++-- .../core/src/session/runner/to-llm-message.ts | 21 ++++--- packages/core/test/aisdk.test.ts | 53 +++++++++++++++- .../core/test/session-runner-message.test.ts | 28 +++++++++ .../core/test/session-runner-model.test.ts | 2 + .../test/session-runner-tool-events.test.ts | 63 +++++++++++++++---- packages/core/test/session-runner.test.ts | 43 ++++++++----- .../llm/src/protocols/anthropic-messages.ts | 1 + .../llm/src/protocols/bedrock-converse.ts | 1 + packages/llm/src/protocols/gemini.ts | 1 + packages/llm/src/protocols/openai-chat.ts | 1 + .../src/protocols/openai-compatible-chat.ts | 1 + .../llm/src/protocols/openai-responses.ts | 2 + packages/llm/src/route/client.ts | 8 +++ 16 files changed, 201 insertions(+), 46 deletions(-) diff --git a/packages/core/src/aisdk.ts b/packages/core/src/aisdk.ts index aecf7a64824..3330c77f4a8 100644 --- a/packages/core/src/aisdk.ts +++ b/packages/core/src/aisdk.ts @@ -305,6 +305,7 @@ function modelFromLanguage(info: ModelV2.Info, language: LanguageModelV3) { const route: AnyRoute = { id: `ai-sdk:${ProviderV2.packageName(info.package) ?? "unknown"}`, provider: ProviderID.make(info.providerID), + providerMetadataKey: optionKey, protocol: "ai-sdk", endpoint: Endpoint.path("/", { baseURL: "https://ai-sdk.local" }), auth: Auth.none, @@ -417,7 +418,7 @@ function assistantPart(part: ContentPart): AssistantContent { case "media": return [{ type: "file", mediaType: part.mediaType, data: part.data, filename: part.filename }] case "reasoning": - return [{ type: "reasoning", text: part.text }] + return [{ type: "reasoning", text: part.text, providerOptions: providerOptions(part.providerMetadata) }] case "tool-call": return [ { @@ -426,6 +427,7 @@ function assistantPart(part: ContentPart): AssistantContent { toolName: part.name, input: part.input, providerExecuted: part.providerExecuted, + providerOptions: providerOptions(part.providerMetadata), }, ] case "tool-result": @@ -441,6 +443,7 @@ function toolResultPart(part: ContentPart): ToolResultContent[] { toolCallId: part.id, toolName: part.name, output: toolOutput(part.result), + providerOptions: providerOptions(part.providerMetadata), }, ] } diff --git a/packages/core/src/session/runner/llm.ts b/packages/core/src/session/runner/llm.ts index dea133ebb9b..d757b19484e 100644 --- a/packages/core/src/session/runner/llm.ts +++ b/packages/core/src/session/runner/llm.ts @@ -233,6 +233,7 @@ const layer = Layer.effect( } const resolved = yield* models.resolve(session) const model = resolved.model + const providerMetadataKey = model.route.providerMetadataKey ?? model.provider const entries = yield* SessionHistory.entriesForRunner(db, session.id, checkpoint.baselineSeq) const context = entries.map((entry) => entry.message) const isLastStep = agent.info?.steps !== undefined && currentStep >= agent.info.steps @@ -250,7 +251,7 @@ const layer = Layer.effect( .filter((part): part is string => part !== undefined && part.length > 0) .map(SystemPart.make), messages: [ - ...toLLMMessages(context, resolved.ref), + ...toLLMMessages(context, resolved.ref, providerMetadataKey), ...(isLastStep ? [Message.assistant(MAX_STEPS_PROMPT)] : []), ], tools: toolMaterialization?.definitions ?? [], @@ -293,7 +294,7 @@ const layer = Layer.effect( // The selected catalog identity, not model.id: route-level ids are provider API // model ids (for example gpt-5.5-fast resolves to api id gpt-5.5). model: resolved.ref, - provider: model.provider, + providerMetadataKey, snapshot: startSnapshot, assistantMessageID, }) diff --git a/packages/core/src/session/runner/publish-llm-event.ts b/packages/core/src/session/runner/publish-llm-event.ts index e7f5454c395..dfd0dca499b 100644 --- a/packages/core/src/session/runner/publish-llm-event.ts +++ b/packages/core/src/session/runner/publish-llm-event.ts @@ -14,7 +14,7 @@ type Input = { readonly sessionID: SessionSchema.ID readonly agent: AgentV2.ID readonly model: ModelV2.Ref - readonly provider: string + readonly providerMetadataKey: string readonly snapshot?: Snapshot.ID readonly assistantMessageID?: SessionMessage.ID } @@ -87,7 +87,9 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input) assistantMessageID ??= SessionMessage.ID.create() stepStarted = true yield* events.publish(SessionEvent.Step.Started, { - ...input, + sessionID: input.sessionID, + agent: input.agent, + model: input.model, assistantMessageID, snapshot: input.snapshot, }) @@ -97,8 +99,7 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input) assistantMessageID === undefined ? Effect.die(new Error("Tool event before assistant step start")) : Effect.succeed(assistantMessageID) - const providerState = (metadata: ProviderMetadata | undefined) => metadata?.[input.provider] - + const providerState = (metadata: ProviderMetadata | undefined) => metadata?.[input.providerMetadataKey] const fragments = ( name: string, ended: (id: string, value: string, ordinal: number, state?: Record) => Effect.Effect, @@ -352,14 +353,13 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input) if (tool.called) return yield* Effect.die(new Error(`Duplicate tool call: ${event.id}`)) tool.called = true tool.providerExecuted = event.providerExecuted === true - const state = providerState(event.providerMetadata) yield* events.publish(SessionEvent.Tool.Called, { sessionID: input.sessionID, assistantMessageID: tool.assistantMessageID, callID: event.id, input: record(event.input), executed: tool.providerExecuted, - state, + state: providerState(event.providerMetadata), }) return } diff --git a/packages/core/src/session/runner/to-llm-message.ts b/packages/core/src/session/runner/to-llm-message.ts index 048374f519b..bd750948780 100644 --- a/packages/core/src/session/runner/to-llm-message.ts +++ b/packages/core/src/session/runner/to-llm-message.ts @@ -113,7 +113,7 @@ const toolResult = (tool: SessionMessage.AssistantTool, providerMetadata: Provid } } -const assistant = (message: SessionMessage.Assistant, model: ModelV2.Ref) => { +const assistant = (message: SessionMessage.Assistant, model: ModelV2.Ref, providerMetadataKey: string) => { const sameModel = String(message.model.providerID) === String(model.providerID) && String(message.model.id) === String(model.id) const reuseProviderMetadata = sameModel && message.error === undefined @@ -125,7 +125,7 @@ const assistant = (message: SessionMessage.Assistant, model: ModelV2.Ref) => { { type: "reasoning", text: item.text, - providerMetadata: providerMetadata(model.providerID, item.state), + providerMetadata: providerMetadata(providerMetadataKey, item.state), }, ] : item.text.length > 0 @@ -133,13 +133,13 @@ const assistant = (message: SessionMessage.Assistant, model: ModelV2.Ref) => { : [] const call = toolCall( item, - reuseProviderMetadata ? providerMetadata(model.providerID, item.providerState) : undefined, + reuseProviderMetadata ? providerMetadata(providerMetadataKey, item.providerState) : undefined, ) if (item.executed !== true) return [call] const result = toolResult( item, reuseProviderMetadata - ? providerMetadata(model.providerID, item.providerResultState ?? item.providerState) + ? providerMetadata(providerMetadataKey, item.providerResultState ?? item.providerState) : undefined, ) return result ? [call, result] : [call] @@ -155,7 +155,7 @@ const assistant = (message: SessionMessage.Assistant, model: ModelV2.Ref) => { toolResult( item, reuseProviderMetadata - ? providerMetadata(model.providerID, item.providerResultState ?? item.providerState) + ? providerMetadata(providerMetadataKey, item.providerResultState ?? item.providerState) : undefined, ), ) @@ -168,7 +168,7 @@ const assistant = (message: SessionMessage.Assistant, model: ModelV2.Ref) => { ] } -function toLLMMessage(message: SessionMessage.Info, model: ModelV2.Ref): Message[] { +function toLLMMessage(message: SessionMessage.Info, model: ModelV2.Ref, providerMetadataKey: string): Message[] { switch (message.type) { case "agent-switched": case "model-switched": @@ -207,7 +207,7 @@ function toLLMMessage(message: SessionMessage.Info, model: ModelV2.Ref): Message }), ] case "assistant": - return assistant(message, model) + return assistant(message, model, providerMetadataKey) case "compaction": if (message.status !== "completed") return [] return [ @@ -232,5 +232,8 @@ ${message.recent} } /** Translate projected V2 Session history into canonical @opencode-ai/llm context. */ -export const toLLMMessages = (messages: readonly SessionMessage.Info[], model: ModelV2.Ref) => - messages.flatMap((message) => toLLMMessage(message, model)) +export const toLLMMessages = ( + messages: readonly SessionMessage.Info[], + model: ModelV2.Ref, + providerMetadataKey: string = model.providerID, +) => messages.flatMap((message) => toLLMMessage(message, model, providerMetadataKey)) diff --git a/packages/core/test/aisdk.test.ts b/packages/core/test/aisdk.test.ts index 40379494276..0a4b7dadce4 100644 --- a/packages/core/test/aisdk.test.ts +++ b/packages/core/test/aisdk.test.ts @@ -2,7 +2,7 @@ import type { LanguageModelV3CallOptions } from "@ai-sdk/provider" import { AISDK } from "@opencode-ai/core/aisdk" import { ModelV2 } from "@opencode-ai/core/model" import { ProviderV2 } from "@opencode-ai/core/provider" -import { LLM } from "@opencode-ai/llm" +import { LLM, Message } from "@opencode-ai/llm" import { LLMClient } from "@opencode-ai/llm/route" import { expect } from "bun:test" import { Effect } from "effect" @@ -67,3 +67,54 @@ it.effect("projects request settings, headers, and body overlays", () => expect(body).toEqual({ safety_setting: "strict" }) }), ) + +it.effect("projects replay metadata onto AI SDK prompt parts", () => + Effect.gen(function* () { + const aisdk = yield* AISDK.Service + yield* aisdk.hook.sdk((event) => { + event.sdk = { languageModel: () => ({ provider: event.model.providerID }) } + }) + + const resolved = yield* aisdk.model(model("@ai-sdk/anthropic")) + expect(resolved.route.providerMetadataKey).toBe("anthropic") + const prepared = yield* LLMClient.prepare( + LLM.request({ + model: resolved, + messages: [ + Message.assistant([ + { type: "reasoning", text: "Think", providerMetadata: { anthropic: { signature: "signed" } } }, + { + type: "tool-call", + id: "hosted", + name: "web_search", + input: { query: "Effect" }, + providerExecuted: true, + providerMetadata: { anthropic: { blockType: "server_tool_use" } }, + }, + ]), + ], + }), + ) + + expect(prepared.body.prompt).toEqual([ + { + role: "assistant", + content: [ + { + type: "reasoning", + text: "Think", + providerOptions: { anthropic: { signature: "signed" } }, + }, + { + type: "tool-call", + toolCallId: "hosted", + toolName: "web_search", + input: { query: "Effect" }, + providerExecuted: true, + providerOptions: { anthropic: { blockType: "server_tool_use" } }, + }, + ], + }, + ]) + }), +) diff --git a/packages/core/test/session-runner-message.test.ts b/packages/core/test/session-runner-message.test.ts index 5910c4e4fbd..c593749f374 100644 --- a/packages/core/test/session-runner-message.test.ts +++ b/packages/core/test/session-runner-message.test.ts @@ -454,6 +454,34 @@ Recent work ]) }) + test("replays flat state under an OpenCode hosted model's route key", () => { + const opencode = ModelV2.Ref.make({ id: ModelV2.ID.make("claude-fable-5"), providerID: ProviderV2.ID.opencode }) + const messages = toLLMMessages( + [ + SessionMessage.Assistant.make({ + id: id("assistant-opencode-reasoning"), + type: "assistant", + agent: build, + model: opencode, + content: [ + SessionMessage.AssistantReasoning.make({ + type: "reasoning", + text: "Think", + state: { signature: "signed" }, + }), + ], + time: { created, completed: created }, + }), + ], + opencode, + "anthropic", + ) + + expect(messages[0]?.content).toEqual([ + { type: "reasoning", text: "Think", providerMetadata: { anthropic: { signature: "signed" } } }, + ]) + }) + test("lowers failed assistant reasoning to text", () => { const messages = toLLMMessages( [ diff --git a/packages/core/test/session-runner-model.test.ts b/packages/core/test/session-runner-model.test.ts index c2e2dd25570..f3c666b4a97 100644 --- a/packages/core/test/session-runner-model.test.ts +++ b/packages/core/test/session-runner-model.test.ts @@ -53,6 +53,7 @@ describe("SessionRunnerModel", () => { expect(resolved).toMatchObject({ id: "api-test-model", provider: "test-provider" }) expect(resolved.route).toMatchObject({ id: "openai-responses", + providerMetadataKey: "openai", endpoint: { baseURL: "https://openai.example/v1" }, defaults: { headers: { "x-test": "header" }, @@ -264,6 +265,7 @@ describe("SessionRunnerModel", () => { expect(resolved.route).toMatchObject({ id: "anthropic-messages", + providerMetadataKey: "anthropic", endpoint: { baseURL: "https://anthropic.example/v1" }, }) }), diff --git a/packages/core/test/session-runner-tool-events.test.ts b/packages/core/test/session-runner-tool-events.test.ts index 4b53ae47291..179818d8523 100644 --- a/packages/core/test/session-runner-tool-events.test.ts +++ b/packages/core/test/session-runner-tool-events.test.ts @@ -14,7 +14,7 @@ import { createLLMEventPublisher } from "@opencode-ai/core/session/runner/publis const sessionID = SessionV2.ID.make("ses_tool_event_test") const base64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB" -const capture = () => { +const capture = (providerMetadataKey = "anthropic") => { const published: Array<{ readonly type: string; readonly data: unknown }> = [] const events = EventV2.Service.of({ publish: (definition, data) => @@ -45,9 +45,9 @@ const capture = () => { agent: AgentV2.ID.make("build"), model: { id: ModelV2.ID.make("model"), - providerID: ProviderV2.ID.make("provider"), + providerID: ProviderV2.ID.opencode, }, - provider: "openai", + providerMetadataKey, }), } } @@ -99,35 +99,76 @@ test("provider-executed success retains its raw provider result", async () => { expect(success?.data).toHaveProperty("result") }) -test("provider state uses the route provider instead of the catalog provider", async () => { +test("provider metadata is flattened using the route key", async () => { const { published, publisher } = capture() await Effect.runPromise( publisher.publish( - LLMEvent.reasoningStart({ id: "reasoning", providerMetadata: { openai: { itemId: "reasoning" } } }), + LLMEvent.reasoningStart({ id: "reasoning", providerMetadata: { anthropic: { signature: "signed" } } }), ), ) expect(published.find((event) => event.type === "session.reasoning.started.1")?.data).toMatchObject({ - state: { itemId: "reasoning" }, + state: { signature: "signed" }, }) }) -test("reasoning state from an empty delta is retained at reasoning end", async () => { +test("reasoning state from start, empty delta, and end is merged", async () => { const { published, publisher } = capture() - await Effect.runPromise(publisher.publish(LLMEvent.reasoningStart({ id: "reasoning" }))) + await Effect.runPromise( + publisher.publish( + LLMEvent.reasoningStart({ id: "reasoning", providerMetadata: { anthropic: { blockType: "thinking" } } }), + ), + ) await Effect.runPromise( publisher.publish( LLMEvent.reasoningDelta({ id: "reasoning", text: "", - providerMetadata: { openai: { signature: "signed" } }, + providerMetadata: { anthropic: { signature: "signed" }, gateway: { traceID: "trace" } }, }), ), ) - await Effect.runPromise(publisher.publish(LLMEvent.reasoningEnd({ id: "reasoning" }))) + await Effect.runPromise( + publisher.publish( + LLMEvent.reasoningEnd({ id: "reasoning", providerMetadata: { anthropic: { stopReason: "tool_use" } } }), + ), + ) expect(published.find((event) => event.type === "session.reasoning.ended.1")?.data).toMatchObject({ - state: { signature: "signed" }, + state: { blockType: "thinking", signature: "signed", stopReason: "tool_use" }, + }) +}) + +test("provider-executed tool metadata is flattened using the route key", async () => { + const { published, publisher } = capture("openai") + await Effect.runPromise( + publisher.publish( + LLMEvent.toolCall({ + id: "hosted", + name: "web_search", + input: { query: "Effect" }, + providerExecuted: true, + providerMetadata: { openai: { itemId: "call" } }, + }), + ), + ) + await Effect.runPromise( + publisher.publish( + LLMEvent.toolResult({ + id: "hosted", + name: "web_search", + result: { type: "json", value: { found: true } }, + providerExecuted: true, + providerMetadata: { openai: { itemId: "result" } }, + }), + ), + ) + + expect(published.find((event) => event.type === "session.tool.called.1")?.data).toMatchObject({ + state: { itemId: "call" }, + }) + expect(published.find((event) => event.type === "session.tool.success.1")?.data).toMatchObject({ + resultState: { itemId: "result" }, }) }) diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index fd34a1954ef..ef2d24445a5 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -1674,7 +1674,7 @@ describe("SessionRunnerLLM", () => { name: "web_search", input: { query: "hello" }, providerExecuted: true, - providerMetadata: { fake: { source: "provider" } }, + providerMetadata: { openai: { source: "provider" } }, }), LLMEvent.toolResult({ id: "call-provider", @@ -1687,7 +1687,7 @@ describe("SessionRunnerLLM", () => { ], }, providerExecuted: true, - providerMetadata: { fake: { source: "provider" } }, + providerMetadata: { openai: { source: "provider" } }, }), LLMEvent.stepFinish({ index: 0, @@ -1834,21 +1834,21 @@ describe("SessionRunnerLLM", () => { LLMEvent.reasoningDelta({ id: "reasoning-anthropic", text: "Signed thought" }), LLMEvent.reasoningEnd({ id: "reasoning-anthropic", - providerMetadata: { fake: { signature: "sig_1" }, anthropic: { ignored: true } }, + providerMetadata: { openai: { signature: "sig_1" }, anthropic: { ignored: true } }, }), LLMEvent.reasoningStart({ id: "reasoning-openai", providerMetadata: { - fake: { itemId: "rs_1", reasoningEncryptedContent: null }, - openai: { ignored: true }, + openai: { itemId: "rs_1", reasoningEncryptedContent: null }, + anthropic: { ignored: true }, }, }), LLMEvent.reasoningDelta({ id: "reasoning-openai", text: "Encrypted thought" }), LLMEvent.reasoningEnd({ id: "reasoning-openai", providerMetadata: { - fake: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" }, - openai: { ignored: true }, + openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" }, + anthropic: { ignored: true }, }, }), LLMEvent.stepFinish({ index: 0, reason: "stop" }), @@ -1862,7 +1862,11 @@ describe("SessionRunnerLLM", () => { { type: "assistant", content: [ - { type: "reasoning", text: "Signed thought", state: { signature: "sig_1" } }, + { + type: "reasoning", + text: "Signed thought", + state: { signature: "sig_1" }, + }, { type: "reasoning", text: "Encrypted thought", @@ -1877,11 +1881,15 @@ describe("SessionRunnerLLM", () => { yield* session.resume(sessionID) expect(requests[1]?.messages[1]?.content).toEqual([ - { type: "reasoning", text: "Signed thought", providerMetadata: { fake: { signature: "sig_1" } } }, + { + type: "reasoning", + text: "Signed thought", + providerMetadata: { openai: { signature: "sig_1" } }, + }, { type: "reasoning", text: "Encrypted thought", - providerMetadata: { fake: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } }, + providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } }, }, ]) }), @@ -1899,14 +1907,14 @@ describe("SessionRunnerLLM", () => { name: "web_search", input: { query: "Effect" }, providerExecuted: true, - providerMetadata: { fake: { itemId: "hosted-search" }, openai: { ignored: true } }, + providerMetadata: { openai: { itemId: "hosted-search" }, fake: { ignored: true } }, }), LLMEvent.toolResult({ id: "hosted-search", name: "web_search", result: { type: "json", value: [{ title: "Effect" }] }, providerExecuted: true, - providerMetadata: { fake: { blockType: "web_search_tool_result" }, anthropic: { ignored: true } }, + providerMetadata: { openai: { blockType: "web_search_tool_result" }, anthropic: { ignored: true } }, }), LLMEvent.stepFinish({ index: 0, reason: "stop" }), LLMEvent.finish({ reason: "stop" }), @@ -1926,7 +1934,7 @@ describe("SessionRunnerLLM", () => { name: "web_search", input: { query: "Effect" }, providerExecuted: true, - providerMetadata: { fake: { itemId: "hosted-search" } }, + providerMetadata: { openai: { itemId: "hosted-search" } }, }, { type: "tool-result", @@ -1934,7 +1942,7 @@ describe("SessionRunnerLLM", () => { name: "web_search", result: { type: "json", value: [{ title: "Effect" }] }, providerExecuted: true, - providerMetadata: { fake: { blockType: "web_search_tool_result" } }, + providerMetadata: { openai: { blockType: "web_search_tool_result" } }, }, ]) }), @@ -2469,7 +2477,7 @@ describe("SessionRunnerLLM", () => { type: "tool-call", id: "call-hosted-interrupted", providerExecuted: true, - providerMetadata: { fake: { itemId: "call-hosted-interrupted" } }, + providerMetadata: { openai: { itemId: "call-hosted-interrupted" } }, }, { type: "tool-result", id: "call-hosted-interrupted", providerExecuted: true, result: { type: "error" } }, ]) @@ -2670,7 +2678,10 @@ describe("SessionRunnerLLM", () => { { type: "tool", id: "call-missing", - state: { status: "error", error: { message: "Unknown tool: missing" } }, + state: { + status: "error", + error: { type: "tool.unknown", message: "Unknown tool: missing" }, + }, }, ], }, diff --git a/packages/llm/src/protocols/anthropic-messages.ts b/packages/llm/src/protocols/anthropic-messages.ts index f6268272922..6117cd5c202 100644 --- a/packages/llm/src/protocols/anthropic-messages.ts +++ b/packages/llm/src/protocols/anthropic-messages.ts @@ -876,6 +876,7 @@ export const protocol = Protocol.make({ export const route = Route.make({ id: ADAPTER, provider: "anthropic", + providerMetadataKey: "anthropic", protocol, endpoint: Endpoint.path(PATH, { baseURL: DEFAULT_BASE_URL }), auth: Auth.none, diff --git a/packages/llm/src/protocols/bedrock-converse.ts b/packages/llm/src/protocols/bedrock-converse.ts index 0c6b0598ffd..4984e323658 100644 --- a/packages/llm/src/protocols/bedrock-converse.ts +++ b/packages/llm/src/protocols/bedrock-converse.ts @@ -657,6 +657,7 @@ export const protocol = Protocol.make({ export const route = Route.make({ id: ADAPTER, provider: "bedrock", + providerMetadataKey: "bedrock", protocol, // Bedrock's URL embeds the region in the route endpoint host and the // validated modelId in the path. We read the validated body so the URL diff --git a/packages/llm/src/protocols/gemini.ts b/packages/llm/src/protocols/gemini.ts index c4bb9476a49..82a69b059ef 100644 --- a/packages/llm/src/protocols/gemini.ts +++ b/packages/llm/src/protocols/gemini.ts @@ -500,6 +500,7 @@ export const protocol = Protocol.make({ export const route = Route.make({ id: ADAPTER, provider: "google", + providerMetadataKey: "google", protocol, // Gemini's path embeds the model id and pins SSE framing at the URL level. endpoint: Endpoint.path(({ request }) => `/models/${request.model.id}:streamGenerateContent?alt=sse`, { diff --git a/packages/llm/src/protocols/openai-chat.ts b/packages/llm/src/protocols/openai-chat.ts index eb3b7141c62..cba67ce0a92 100644 --- a/packages/llm/src/protocols/openai-chat.ts +++ b/packages/llm/src/protocols/openai-chat.ts @@ -493,6 +493,7 @@ export const httpTransport = HttpTransport.sseJson.with() export const route = Route.make({ id: ADAPTER, provider: "openai", + providerMetadataKey: "openai", protocol, endpoint: Endpoint.path(PATH, { baseURL: DEFAULT_BASE_URL }), auth: Auth.none, diff --git a/packages/llm/src/protocols/openai-compatible-chat.ts b/packages/llm/src/protocols/openai-compatible-chat.ts index ce3f0a83d75..9ae9a53b437 100644 --- a/packages/llm/src/protocols/openai-compatible-chat.ts +++ b/packages/llm/src/protocols/openai-compatible-chat.ts @@ -16,6 +16,7 @@ export type OpenAICompatibleChatModelInput = RouteRoutedModelInput */ export const route = Route.make({ id: ADAPTER, + providerMetadataKey: "openai", protocol: OpenAIChat.protocol, endpoint: Endpoint.path("/chat/completions"), framing: Framing.sse, diff --git a/packages/llm/src/protocols/openai-responses.ts b/packages/llm/src/protocols/openai-responses.ts index e0546f47ed0..e2889d5b6d6 100644 --- a/packages/llm/src/protocols/openai-responses.ts +++ b/packages/llm/src/protocols/openai-responses.ts @@ -990,6 +990,7 @@ export const httpTransport = HttpTransport.sseJson.with() export const route = Route.make({ id: ADAPTER, provider: "openai", + providerMetadataKey: "openai", protocol, endpoint, auth, @@ -1018,6 +1019,7 @@ export const webSocketTransport = WebSocketTransport.jsonTransport.with< export const webSocketRoute = Route.make({ id: `${ADAPTER}-websocket`, provider: "openai", + providerMetadataKey: "openai", protocol, endpoint, auth, diff --git a/packages/llm/src/route/client.ts b/packages/llm/src/route/client.ts index d3b41f5817f..a258aae9448 100644 --- a/packages/llm/src/route/client.ts +++ b/packages/llm/src/route/client.ts @@ -36,6 +36,8 @@ export interface RouteBody { export interface Route { readonly id: string readonly provider?: ProviderID + /** ProviderMetadata namespace emitted and consumed by this route. */ + readonly providerMetadataKey?: string readonly protocol: ProtocolID readonly endpoint: Endpoint readonly auth: AuthDef @@ -184,6 +186,8 @@ export interface MakeInput { readonly id: string /** Provider identity for route-owned model construction. */ readonly provider?: string | ProviderID + /** ProviderMetadata namespace emitted and consumed by this route. */ + readonly providerMetadataKey?: string /** Semantic API contract — owns body construction, body schema, and parsing. */ readonly protocol: Protocol /** Where the request is sent. */ @@ -203,6 +207,8 @@ export interface MakeTransportInput { readonly id: string /** Provider identity for route-owned model construction. */ readonly provider?: string | ProviderID + /** ProviderMetadata namespace emitted and consumed by this route. */ + readonly providerMetadataKey?: string /** Semantic API contract — owns body construction, body schema, and parsing. */ readonly protocol: Protocol /** Where the request is sent. */ @@ -248,6 +254,7 @@ function makeFromTransport( const route: Route = { id: routeInput.id, provider: routeInput.provider === undefined ? undefined : ProviderID.make(routeInput.provider), + providerMetadataKey: routeInput.providerMetadataKey, protocol: protocol.id, endpoint: routeInput.endpoint, auth: routeInput.auth ?? Auth.none, @@ -329,6 +336,7 @@ export function make( return makeFromTransport({ id: input.id, provider: input.provider, + providerMetadataKey: input.providerMetadataKey, protocol, endpoint: input.endpoint, auth: input.auth,