Compare commits

...
1 Commits
Author SHA1 Message Date
Aiden Cline f240fbb8a6 refactor(ai): namespace model metadata by route ID 2026-09-07 00:48:56 -05:00
65 changed files with 739 additions and 570 deletions
+2
View File
@@ -81,6 +81,8 @@ export const route = Route.make({
Route defaults are request-shaping defaults such as `headers`, `limits`, `generation`, `providerOptions`, and `http`. Endpoint host/query belongs on the route endpoint. Selected `LanguageModel` values carry only model id, provider id, and the configured route value. Model capability/catalog metadata lives outside this package; protocol support is enforced by request lowering and typed `AIError`s.
LLM `ProviderMetadata` is indexed by the selected `LanguageModel.route.id`. Use that same route ID when emitting usage, reasoning signatures, item IDs, and hosted-tool state and when reading them for continuation. There is no independently configurable metadata namespace. Reassigning a provider does not rename the route or its metadata; changing the route ID changes both. Provider-specific APIs with different replay formats must have distinct route IDs.
The four-axis decomposition is the reason DeepSeek, TogetherAI, Cerebras, Baseten, Fireworks, and DeepInfra all reuse `OpenAIChat.protocol` verbatim — each provider owns a small `Route.make(...)` composition instead of a protocol clone. Bug fixes in one protocol propagate to every consumer of that protocol in a single commit.
When a provider supports multiple physical transports, selection remains execution policy below its semantic route. `OpenResponsesChannel.transport(...)` owns the provider-neutral Responses WebSocket concept: it prepares one final request, executes HTTP by default, strips WebSocket-disallowed fields, and passes a generic channel exchange to a per-call `WebSocketChannelExecutor` when supplied. Provider-specific Responses routes opt in with handshake and connection-age policy. `Route.streamPrepared` owns decoding and acknowledges channel completion only after successful full consumption.
+29
View File
@@ -534,6 +534,35 @@ Additional provider entrypoints include:
- `@opencode-ai/ai/providers/cloudflare-ai-gateway`
- `@opencode-ai/ai/providers/cloudflare-workers-ai`
### Route-keyed metadata
LLM messages, events, and usage retain provider-specific information under `providerMetadata[model.route.id]`. The provider ID identifies the provider; the route ID identifies its API and owns the metadata needed to replay that API's responses.
```ts
const model = GoogleVertexMessages.configure({ project, accessToken }).model("claude-sonnet-4-5")
const response = yield * LLM.generate(LLM.request({ model, prompt: "Think through this problem." }))
const reasoning = response.message.content.find((part) => part.type === "reasoning")
const signature = reasoning?.providerMetadata?.[model.route.id]?.signature
// model.route.id === "google-vertex-messages"
```
The former `providerMetadataKey` route option has been removed. Existing application-owned LLM metadata must use route keys instead of provider names, for example:
| API | Metadata key |
| ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ |
| OpenAI Chat / Responses | `openai-chat` / `openai-responses` |
| Azure Chat / Responses | `azure-openai-chat` / `azure-openai-responses` |
| Anthropic Messages | `anthropic-messages` |
| Google Gemini | `gemini` |
| Vertex Gemini / Chat / Responses / Messages | `google-vertex-gemini` / `google-vertex-chat` / `google-vertex-responses` / `google-vertex-messages` |
| Bedrock Converse / Mantle Chat / Mantle Responses | `bedrock-converse` / `bedrock-mantle-chat` / `bedrock-mantle-responses` |
| Groq / Mistral | `groq-chat` / `mistral-chat` |
| xAI Chat / Responses | `xai-chat` / `xai-responses` |
| Baseten / Cerebras / DeepInfra / DeepSeek / Fireworks / TogetherAI | `baseten-chat` / `cerebras-chat` / `deepinfra-chat` / `deepseek-chat` / `fireworks-chat` / `togetherai-chat` |
| Generic Chat / Responses | `openai-compatible-chat` / `openai-compatible-responses` |
OpenRouter and Cloudflare already use matching provider and route IDs. Generic Messages uses `anthropic-messages`. Custom routes use their own `id`.
## Provider options & HTTP overlays
Request options in order of stability:
+23 -33
View File
@@ -455,7 +455,7 @@ type AnthropicEvent = Schema.Schema.Type<typeof AnthropicEvent>
interface ParserState {
readonly provider: LLMRequest["model"]["provider"]
readonly compactions: Readonly<Record<number, string | null>>
readonly providerMetadataKey: string
readonly routeID: string
readonly tools: ToolStream.State<number>
readonly reasoningSignatures: Readonly<Record<number, string>>
readonly usage?: Usage
@@ -563,14 +563,14 @@ const serverToolResultType = (name: string): AnthropicServerToolResultType | und
const lowerServerToolResult = Effect.fn("AnthropicMessages.lowerServerToolResult")(function* (
part: ToolResultPart,
providerMetadataKey: string,
routeID: string,
) {
const wireType = serverToolResultType(part.name)
if (!wireType)
return yield* invalid(`Anthropic Messages does not know how to round-trip server tool result for ${part.name}`)
// Prefer the provider-owned replay payload; fall back to the result value for
// histories constructed directly from provider events.
const payload = part.providerMetadata?.[providerMetadataKey]?.["result"] ?? part.result.value
const payload = part.providerMetadata?.[routeID]?.["result"] ?? part.result.value
return {
type: wireType,
tool_use_id: scrubToolCallID(part.id),
@@ -857,7 +857,7 @@ const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (
breakpoints: Cache.Breakpoints,
) {
const messages: AnthropicMessage[] = []
const providerMetadataKey = request.model.route.providerMetadataKey ?? String(request.model.provider)
const routeID = request.model.route.id
for (const [index, message] of request.messages.entries()) {
if (message.role === "system") {
@@ -911,8 +911,8 @@ const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (
if (part.type === "reasoning") {
// A signature marks visible thinking; only signature-less parts carrying
// redactedData round-trip as opaque redacted_thinking blocks.
const signature = part.encrypted ?? signatureFromMetadata(part.providerMetadata, providerMetadataKey)
const redactedData = redactedDataFromMetadata(part.providerMetadata, providerMetadataKey)
const signature = part.encrypted ?? signatureFromMetadata(part.providerMetadata, routeID)
const redactedData = redactedDataFromMetadata(part.providerMetadata, routeID)
if (signature === undefined && redactedData !== undefined) {
content.push({ type: "redacted_thinking", data: redactedData })
continue
@@ -941,7 +941,7 @@ const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (
continue
}
if (part.type === "tool-result" && part.providerExecuted) {
content.push(yield* lowerServerToolResult(part, providerMetadataKey))
content.push(yield* lowerServerToolResult(part, routeID))
continue
}
return yield* invalid(
@@ -1148,7 +1148,7 @@ const mapFinishReason = (reason: string | null | undefined): FinishReason => {
// inclusive `inputTokens` the rest of the contract expects. Extended
// thinking tokens are included in `output_tokens`; newer responses also
// expose that subset through `output_tokens_details.thinking_tokens`.
const mapUsage = (usage: AnthropicUsage | undefined, providerMetadataKey: string): Usage | undefined => {
const mapUsage = (usage: AnthropicUsage | undefined, routeID: string): Usage | undefined => {
if (!usage) return undefined
const iterations = usage.iterations?.length ? usage.iterations : [usage]
const last = usage.iterations?.at(-1)
@@ -1175,7 +1175,7 @@ const mapUsage = (usage: AnthropicUsage | undefined, providerMetadataKey: string
cacheWriteInputTokens: cacheWrite,
reasoningTokens: ProviderShared.sumTokens(...iterations.map((item) => item.output_tokens_details?.thinking_tokens)),
totalTokens: ProviderShared.totalTokens(inputTokens, outputTokens, undefined),
providerMetadata: { [providerMetadataKey]: usage },
providerMetadata: { [routeID]: usage },
})
}
@@ -1184,7 +1184,7 @@ const mapUsage = (usage: AnthropicUsage | undefined, providerMetadataKey: string
// field prefers `right` when defined, falls back to `left`. `inputTokens` is
// recomputed from the merged breakdown so the inclusive total stays
// consistent with `nonCached + cacheRead + cacheWrite`.
const mergeUsage = (left: Usage | undefined, right: Usage | undefined, providerMetadataKey: string) => {
const mergeUsage = (left: Usage | undefined, right: Usage | undefined, routeID: string) => {
if (!left) return right
if (!right) return left
const nonCachedInputTokens = right.nonCachedInputTokens ?? left.nonCachedInputTokens
@@ -1203,9 +1203,7 @@ const mergeUsage = (left: Usage | undefined, right: Usage | undefined, providerM
reasoningTokens,
totalTokens: ProviderShared.totalTokens(inputTokens, outputTokens, undefined),
providerMetadata: {
[providerMetadataKey]:
mergeJsonRecords(left.providerMetadata?.[providerMetadataKey], right.providerMetadata?.[providerMetadataKey]) ??
{},
[routeID]: mergeJsonRecords(left.providerMetadata?.[routeID], right.providerMetadata?.[routeID]) ?? {},
},
})
}
@@ -1223,7 +1221,7 @@ const SERVER_TOOL_RESULT_NAMES: Record<AnthropicServerToolResultType, string> =
const isServerToolResultType = (type: string): type is AnthropicServerToolResultType => type in SERVER_TOOL_RESULT_NAMES
const serverToolResultEvent = (block: AnthropicStreamBlock, providerMetadataKey: string): LLMEvent | undefined => {
const serverToolResultEvent = (block: AnthropicStreamBlock, routeID: string): LLMEvent | undefined => {
if (!block.type || !isServerToolResultType(block.type)) return undefined
const errorPayload =
typeof block.content === "object" && block.content !== null && "type" in block.content
@@ -1237,7 +1235,7 @@ const serverToolResultEvent = (block: AnthropicStreamBlock, providerMetadataKey:
providerExecuted: true,
// The complete payload is irreducible provider replay state: subsequent
// stateless requests must round-trip the typed result block verbatim.
providerMetadata: providerMetadata(providerMetadataKey, { blockType: block.type, result: block.content }),
providerMetadata: providerMetadata(routeID, { blockType: block.type, result: block.content }),
})
}
@@ -1246,8 +1244,8 @@ type StepResult = readonly [ParserState, ReadonlyArray<LLMEvent>]
const NO_EVENTS: StepResult["1"] = []
const onMessageStart = (state: ParserState, event: AnthropicEvent): StepResult => {
const usage = mapUsage(event.message?.usage, state.providerMetadataKey)
return [usage ? { ...state, usage: mergeUsage(state.usage, usage, state.providerMetadataKey) } : state, NO_EVENTS]
const usage = mapUsage(event.message?.usage, state.routeID)
return [usage ? { ...state, usage: mergeUsage(state.usage, usage, state.routeID) } : state, NO_EVENTS]
}
const onContentBlockStart = (
@@ -1299,9 +1297,7 @@ const onContentBlockStart = (
const events: LLMEvent[] = []
const id = `reasoning-${event.index ?? 0}`
const metadata =
block.signature === undefined
? undefined
: providerMetadata(state.providerMetadataKey, { signature: block.signature })
block.signature === undefined ? undefined : providerMetadata(state.routeID, { signature: block.signature })
const lifecycle = Lifecycle.reasoningStart(state.lifecycle, events, id, metadata)
return [
{
@@ -1330,14 +1326,14 @@ const onContentBlockStart = (
state.lifecycle,
events,
`reasoning-${event.index ?? 0}`,
providerMetadata(state.providerMetadataKey, { redactedData: block.data }),
providerMetadata(state.routeID, { redactedData: block.data }),
),
},
events,
]
}
const result = serverToolResultEvent(block, state.providerMetadataKey)
const result = serverToolResultEvent(block, state.routeID)
if (!result) return [state, NO_EVENTS]
const events: LLMEvent[] = []
return [{ ...state, lifecycle: Lifecycle.stepStart(state.lifecycle, events) }, [...events, result]]
@@ -1438,7 +1434,7 @@ const onContentBlockStop = Effect.fn("AnthropicMessages.onContentBlockStop")(fun
Lifecycle.textEnd(state.lifecycle, events, `text-${event.index}`),
events,
`reasoning-${event.index}`,
signature === undefined ? undefined : providerMetadata(state.providerMetadataKey, { signature }),
signature === undefined ? undefined : providerMetadata(state.routeID, { signature }),
)
events.push(...resultEvents)
const reasoningSignatures = { ...state.reasoningSignatures }
@@ -1450,7 +1446,7 @@ const onMessageDelta = (
state: ParserState,
event: AnthropicEvent & { readonly delta?: AnthropicStreamDelta },
): StepResult => {
const usage = mergeUsage(state.usage, mapUsage(event.usage, state.providerMetadataKey), state.providerMetadataKey)
const usage = mergeUsage(state.usage, mapUsage(event.usage, state.routeID), state.routeID)
const pendingFinish = (() => {
const stopReason = event.delta?.stop_reason
if (stopReason === null || stopReason === undefined) return state.pendingFinish
@@ -1459,7 +1455,7 @@ const onMessageDelta = (
const finishMetadata =
stopSequence === null || stopSequence === undefined
? state.pendingFinish?.providerMetadata
: providerMetadata(state.providerMetadataKey, { stopSequence })
: providerMetadata(state.routeID, { stopSequence })
return {
reason: {
normalized: mapFinishReason(stopReason),
@@ -1487,12 +1483,7 @@ const onMessageStop = Effect.fn("AnthropicMessages.onMessageStop")(function* (st
events.push(...result.events)
const closed = Object.entries(state.reasoningSignatures).reduce(
(current, [index, signature]) =>
Lifecycle.reasoningEnd(
current,
events,
`reasoning-${index}`,
providerMetadata(state.providerMetadataKey, { signature }),
),
Lifecycle.reasoningEnd(current, events, `reasoning-${index}`, providerMetadata(state.routeID, { signature })),
lifecycle,
)
const finished = Lifecycle.finish(closed, events, {
@@ -1627,7 +1618,7 @@ export const protocol = Protocol.make({
initial: (request) => ({
provider: request.model.provider,
compactions: {},
providerMetadataKey: request.model.route.providerMetadataKey ?? String(request.model.provider),
routeID: request.model.route.id,
tools: ToolStream.empty<number>(),
reasoningSignatures: {},
lifecycle: Lifecycle.initial(),
@@ -1670,7 +1661,6 @@ export const transport = <Body extends Pick<AnthropicMessagesBody, "messages" |
export const route = Route.make({
id: ADAPTER,
provider: "anthropic",
providerMetadataKey: "anthropic",
protocol,
endpoint: Endpoint.path((input) => (input.request.model.provider === "anthropic" ? `${PATH}?beta=true` : PATH), {
baseURL: DEFAULT_BASE_URL,
+16 -17
View File
@@ -260,14 +260,14 @@ const lowerToolChoice = (toolChoice: NonNullable<LLMRequest["toolChoice"]>) =>
const providerMetadata = (key: string, metadata: Record<string, unknown>): ProviderMetadata => ({ [key]: metadata })
const reasoningSignature = (part: ReasoningPart, providerMetadataKey: string) => {
const metadata = part.providerMetadata?.[providerMetadataKey]
const reasoningSignature = (part: ReasoningPart, routeID: string) => {
const metadata = part.providerMetadata?.[routeID]
if (part.encrypted !== undefined) return part.encrypted
if (ProviderShared.isRecord(metadata) && typeof metadata.signature === "string") return metadata.signature
}
const reasoningRedactedData = (part: ReasoningPart, providerMetadataKey: string) => {
const metadata = part.providerMetadata?.[providerMetadataKey]
const reasoningRedactedData = (part: ReasoningPart, routeID: string) => {
const metadata = part.providerMetadata?.[routeID]
if (ProviderShared.isRecord(metadata) && typeof metadata.redactedData === "string") return metadata.redactedData
}
@@ -324,7 +324,7 @@ const lowerMessages = Effect.fn("BedrockConverse.lowerMessages")(function* (
breakpoints: BedrockCache.Breakpoints,
) {
const messages: BedrockMessage[] = []
const providerMetadataKey = request.model.route.providerMetadataKey ?? String(request.model.provider)
const routeID = request.model.route.id
for (const message of request.messages) {
if (message.role === "system") {
@@ -372,8 +372,8 @@ const lowerMessages = Effect.fn("BedrockConverse.lowerMessages")(function* (
continue
}
if (part.type === "reasoning") {
const signature = reasoningSignature(part, providerMetadataKey)
const redactedData = reasoningRedactedData(part, providerMetadataKey)
const signature = reasoningSignature(part, routeID)
const redactedData = reasoningRedactedData(part, routeID)
if (signature === undefined && redactedData !== undefined) {
content.push({ reasoningContent: { redactedContent: redactedData } })
continue
@@ -485,7 +485,7 @@ const mapFinishReason = (reason: string): FinishReason => {
// AWS reports inputTokens separately from cache reads and writes.
// Bedrock does not break reasoning out of outputTokens for current models.
const mapUsage = (usage: BedrockUsageSchema | undefined, providerMetadataKey: string): Usage | undefined => {
const mapUsage = (usage: BedrockUsageSchema | undefined, routeID: string): Usage | undefined => {
if (!usage) return undefined
const inputTokens = ProviderShared.sumTokens(
usage.inputTokens,
@@ -499,12 +499,12 @@ const mapUsage = (usage: BedrockUsageSchema | undefined, providerMetadataKey: st
cacheReadInputTokens: usage.cacheReadInputTokens,
cacheWriteInputTokens: usage.cacheWriteInputTokens,
totalTokens: ProviderShared.totalTokens(inputTokens, usage.outputTokens, usage.totalTokens),
providerMetadata: { [providerMetadataKey]: usage },
providerMetadata: { [routeID]: usage },
})
}
interface ParserState {
readonly providerMetadataKey: string
readonly routeID: string
readonly tools: ToolStream.State<number>
// Bedrock splits the finish into `messageStop` (carries `stopReason`) and
// `metadata` (carries usage). Hold both in state so `onHalt` can emit exactly
@@ -587,8 +587,8 @@ const step = (state: ParserState, event: BedrockEvent) =>
})()
const redactedData = redactedChunks === undefined ? reasoning.data : encodeRedactedContent(redactedChunks)
const metadata = (() => {
if (reasoning.signature) return providerMetadata(state.providerMetadataKey, { signature: reasoning.signature })
if (redactedData !== undefined) return providerMetadata(state.providerMetadataKey, { redactedData })
if (reasoning.signature) return providerMetadata(state.routeID, { signature: reasoning.signature })
if (redactedData !== undefined) return providerMetadata(state.routeID, { redactedData })
})()
const lifecycle = (() => {
if (reasoning.text === undefined && metadata === undefined) return state.lifecycle
@@ -640,10 +640,10 @@ const step = (state: ParserState, event: BedrockEvent) =>
if (resultEvents.length) return Lifecycle.stepStart(state.lifecycle, events)
const metadata = (() => {
const signature = state.reasoningSignatures[index]
if (signature) return providerMetadata(state.providerMetadataKey, { signature })
if (signature) return providerMetadata(state.routeID, { signature })
const redactedContent = state.reasoningRedactedContent[index]
if (redactedContent)
return providerMetadata(state.providerMetadataKey, {
return providerMetadata(state.routeID, {
redactedData: encodeRedactedContent(redactedContent),
})
})()
@@ -697,7 +697,7 @@ const step = (state: ParserState, event: BedrockEvent) =>
}
if (event.metadata) {
const usage = mapUsage(event.metadata.usage, state.providerMetadataKey) ?? state.usage
const usage = mapUsage(event.metadata.usage, state.routeID) ?? state.usage
return [
{
...state,
@@ -757,7 +757,7 @@ export const protocol = Protocol.make({
stream: {
event: BedrockEvent,
initial: (request) => ({
providerMetadataKey: request.model.route.providerMetadataKey ?? String(request.model.provider),
routeID: request.model.route.id,
tools: ToolStream.empty<number>(),
finishReason: undefined,
usage: undefined,
@@ -774,7 +774,6 @@ 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
+14 -23
View File
@@ -232,7 +232,7 @@ type GeminiEvent = Schema.Schema.Type<typeof GeminiEvent>
interface ParserState {
readonly route: string
readonly providerMetadataKey: string
readonly routeID: string
readonly finishReason?: string
readonly hasToolCalls: boolean
readonly promptFeedback?: GeminiPromptFeedback
@@ -309,7 +309,7 @@ const lowerToolCall = (part: ToolCallPart, omitIds: boolean, metadataKey: string
const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request: LLMRequest) {
const contents: GeminiContent[] = []
const metadataKey = request.model.route.providerMetadataKey ?? String(request.model.provider)
const metadataKey = request.model.route.id
const omitCallIds = omitsFunctionCallIds(request.model.id)
const legacyToolMedia = routesLegacyToolMedia(request.model.id)
let pendingMedia: GeminiInlineDataPart[] | undefined
@@ -583,7 +583,7 @@ const finish = (state: ParserState): ReadonlyArray<LLMEvent> => {
state.reasoningId,
state.reasoningSignature === undefined
? undefined
: providerMetadata(state.providerMetadataKey, { thoughtSignature: state.reasoningSignature }),
: providerMetadata(state.routeID, { thoughtSignature: state.reasoningSignature }),
)
if (state.textId !== undefined)
lifecycle = Lifecycle.textEnd(
@@ -592,7 +592,7 @@ const finish = (state: ParserState): ReadonlyArray<LLMEvent> => {
state.textId,
state.textSignature === undefined
? undefined
: providerMetadata(state.providerMetadataKey, { thoughtSignature: state.textSignature }),
: providerMetadata(state.routeID, { thoughtSignature: state.textSignature }),
)
Lifecycle.finish(lifecycle, events, {
reason: {
@@ -604,7 +604,7 @@ const finish = (state: ParserState): ReadonlyArray<LLMEvent> => {
providerMetadata:
state.promptFeedback === undefined
? undefined
: providerMetadata(state.providerMetadataKey, { promptFeedback: state.promptFeedback }),
: providerMetadata(state.routeID, { promptFeedback: state.promptFeedback }),
})
return events
}
@@ -634,9 +634,7 @@ const step = (state: ParserState, event: GeminiEvent) => {
const nextState = {
...state,
promptFeedback: event.promptFeedback ?? state.promptFeedback,
usage: event.usageMetadata
? (mapUsage(event.usageMetadata, state.providerMetadataKey) ?? state.usage)
: state.usage,
usage: event.usageMetadata ? (mapUsage(event.usageMetadata, state.routeID) ?? state.usage) : state.usage,
}
const candidate = event.candidates?.[0]
if (candidate?.finishReason && mapFinishReason(candidate.finishReason, state.hasToolCalls) === "error")
@@ -692,9 +690,7 @@ const step = (state: ParserState, event: GeminiEvent) => {
lifecycle,
events,
textId,
textSignature
? providerMetadata(state.providerMetadataKey, { thoughtSignature: textSignature })
: undefined,
textSignature ? providerMetadata(state.routeID, { thoughtSignature: textSignature }) : undefined,
)
textId = undefined
textSignature = undefined
@@ -708,7 +704,7 @@ const step = (state: ParserState, event: GeminiEvent) => {
events,
reasoningId,
part.text,
signature ? providerMetadata(state.providerMetadataKey, { thoughtSignature: signature }) : undefined,
signature ? providerMetadata(state.routeID, { thoughtSignature: signature }) : undefined,
)
continue
}
@@ -717,9 +713,7 @@ const step = (state: ParserState, event: GeminiEvent) => {
lifecycle,
events,
reasoningId,
reasoningSignature
? providerMetadata(state.providerMetadataKey, { thoughtSignature: reasoningSignature })
: undefined,
reasoningSignature ? providerMetadata(state.routeID, { thoughtSignature: reasoningSignature }) : undefined,
)
reasoningId = undefined
reasoningSignature = undefined
@@ -733,7 +727,7 @@ const step = (state: ParserState, event: GeminiEvent) => {
events,
textId,
part.text,
textSignature ? providerMetadata(state.providerMetadataKey, { thoughtSignature: textSignature }) : undefined,
textSignature ? providerMetadata(state.routeID, { thoughtSignature: textSignature }) : undefined,
)
textSignature = undefined
continue
@@ -754,9 +748,7 @@ const step = (state: ParserState, event: GeminiEvent) => {
lifecycle,
events,
reasoningId,
reasoningSignature
? providerMetadata(state.providerMetadataKey, { thoughtSignature: reasoningSignature })
: undefined,
reasoningSignature ? providerMetadata(state.routeID, { thoughtSignature: reasoningSignature }) : undefined,
)
reasoningId = undefined
reasoningSignature = undefined
@@ -766,7 +758,7 @@ const step = (state: ParserState, event: GeminiEvent) => {
lifecycle,
events,
textId,
textSignature ? providerMetadata(state.providerMetadataKey, { thoughtSignature: textSignature }) : undefined,
textSignature ? providerMetadata(state.routeID, { thoughtSignature: textSignature }) : undefined,
)
textId = undefined
textSignature = undefined
@@ -778,7 +770,7 @@ const step = (state: ParserState, event: GeminiEvent) => {
name: part.functionCall.name,
input,
providerMetadata: part.thoughtSignature
? providerMetadata(state.providerMetadataKey, { thoughtSignature: part.thoughtSignature })
? providerMetadata(state.routeID, { thoughtSignature: part.thoughtSignature })
: undefined,
}),
)
@@ -821,7 +813,7 @@ export const protocol = Protocol.make({
event: Protocol.jsonEvent(GeminiEvent),
initial: (request) => ({
route: `${request.model.provider}/${request.model.route.id}`,
providerMetadataKey: request.model.route.providerMetadataKey ?? String(request.model.provider),
routeID: request.model.route.id,
hasToolCalls: false,
lifecycle: Lifecycle.initial(),
nextReasoningId: 0,
@@ -835,7 +827,6 @@ 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`, {
+30 -13
View File
@@ -287,9 +287,10 @@ const lowerAssistant = Effect.fn("MistralChat.lowerAssistant")(function* (
message: LLMRequest["messages"][number],
normalizeID: (id: string) => string,
prefix: boolean,
routeID: string,
) {
const structured = message.content.some(
(part) => part.type === "reasoning" && isMistralThinkingContent(part.providerMetadata?.mistral?.thinking),
(part) => part.type === "reasoning" && isMistralThinkingContent(part.providerMetadata?.[routeID]?.thinking),
)
const content: Array<Schema.Schema.Type<typeof MistralTextContent> | MistralThinkingContent> = []
const text: string[] = []
@@ -301,7 +302,7 @@ const lowerAssistant = Effect.fn("MistralChat.lowerAssistant")(function* (
continue
}
if (part.type === "reasoning") {
const native = part.providerMetadata?.mistral?.thinking
const native = part.providerMetadata?.[routeID]?.thinking
if (structured && isMistralThinkingContent(native)) content.push(native)
else if (structured) content.push({ type: "text", text: part.text })
else text.push(part.text)
@@ -378,13 +379,22 @@ const lowerMessages = Effect.fn("MistralChat.lowerMessages")(function* (request:
if (message.role === "assistant") {
const hasToolCalls = message.content.some((part) => part.type === "tool-call")
const hasNativeThinking = message.content.some(
(part) => part.type === "reasoning" && isMistralThinkingContent(part.providerMetadata?.mistral?.thinking),
(part) =>
part.type === "reasoning" &&
isMistralThinkingContent(part.providerMetadata?.[request.model.route.id]?.thinking),
)
const text = message.content
.flatMap((part) => (part.type === "text" || part.type === "reasoning" ? [part.text] : []))
.join("")
if (!hasToolCalls && !hasNativeThinking && text.trim() === "") continue
messages.push(yield* lowerAssistant(message, normalizeID, !hasToolCalls && message === request.messages.at(-1)))
messages.push(
yield* lowerAssistant(
message,
normalizeID,
!hasToolCalls && message === request.messages.at(-1),
request.model.route.id,
),
)
continue
}
messages.push(...(yield* lowerToolResults(message, normalizeID)))
@@ -455,6 +465,7 @@ interface ActiveContent {
}
export interface ParserState {
readonly routeID: string
readonly tools: ToolStream.State<ToolKey>
readonly pendingTools: Partial<Record<ToolKey, PendingTool>>
readonly toolIDs: ReadonlyMap<string, string>
@@ -469,7 +480,7 @@ export interface ParserState {
readonly finishReason?: FinishReasonDetails
}
const mapUsage = (usage: MistralEvent["usage"]): Usage | undefined => {
const mapUsage = (usage: MistralEvent["usage"], routeID: string): Usage | undefined => {
if (!usage) return undefined
const input = usage.prompt_tokens ?? undefined
const reported =
@@ -485,7 +496,7 @@ const mapUsage = (usage: MistralEvent["usage"]): Usage | undefined => {
nonCachedInputTokens: ProviderShared.subtractTokens(input, cached),
cacheReadInputTokens: cached,
totalTokens: ProviderShared.totalTokens(input, output, usage.total_tokens ?? undefined),
providerMetadata: { mistral: usage },
providerMetadata: { [routeID]: usage },
})
}
@@ -517,7 +528,7 @@ const thinkingUnits = (value: unknown): ReadonlyArray<MistralThinkingUnit> => {
const thinkingText = (thinking: ReadonlyArray<MistralThinkingUnit>) =>
thinking.flatMap((unit) => (typeof unit.text === "string" ? [unit.text] : [])).join("")
const thinkingMetadata = (thinking: MistralThinkingContent) => ({ mistral: { thinking } })
const thinkingMetadata = (thinking: MistralThinkingContent, routeID: string) => ({ [routeID]: { thinking } })
const closeActive = (state: ParserState, events: LLMEvent[]) => {
if (!state.active) return state
@@ -528,7 +539,7 @@ const closeActive = (state: ParserState, events: LLMEvent[]) => {
state.lifecycle,
events,
state.active.id,
thinkingMetadata(state.active.thinking ?? { type: "thinking", thinking: [] }),
thinkingMetadata(state.active.thinking ?? { type: "thinking", thinking: [] }, state.routeID),
thinkingText(state.active.thinking?.thinking ?? []),
)
return { ...state, lifecycle, active: undefined }
@@ -561,8 +572,14 @@ const appendThinking = (state: ParserState, events: LLMEvent[], part: MistralOut
...current,
lifecycle:
text.length > 0
? Lifecycle.reasoningDelta(current.lifecycle, events, active.id, text, thinkingMetadata(thinking))
: Lifecycle.reasoningStart(current.lifecycle, events, active.id, thinkingMetadata(thinking)),
? Lifecycle.reasoningDelta(
current.lifecycle,
events,
active.id,
text,
thinkingMetadata(thinking, state.routeID),
)
: Lifecycle.reasoningStart(current.lifecycle, events, active.id, thinkingMetadata(thinking, state.routeID)),
active: { ...active, thinking },
nextContent: current.active ? current.nextContent : current.nextContent + 1,
}
@@ -683,7 +700,7 @@ const step = Effect.fn("MistralChat.step")(function* (state: ParserState, event:
})
}
const events: LLMEvent[] = []
const usage = mapUsage(event.usage) ?? state.usage
const usage = mapUsage(event.usage, state.routeID) ?? state.usage
if (state.finishReason) {
if (hasLateContent(event))
return yield* ProviderShared.eventError(
@@ -761,7 +778,8 @@ export const protocol = Protocol.make({
body: { schema: MistralBody, from: fromRequest },
stream: {
event: MistralStreamEvent,
initial: (): ParserState => ({
initial: (request): ParserState => ({
routeID: request.model.route.id,
tools: ToolStream.empty<ToolKey>(),
pendingTools: {},
toolIDs: new Map(),
@@ -783,7 +801,6 @@ export const httpTransport = HttpTransport.sseJson.with<MistralBody>().with({ fr
export const route = Route.make({
id: ADAPTER,
provider: "mistral",
providerMetadataKey: "mistral",
protocol,
endpoint: Endpoint.path(PATH, { baseURL: DEFAULT_BASE_URL }),
auth: Auth.none,
+21 -23
View File
@@ -423,7 +423,7 @@ export interface ParserState {
readonly completedCompactions: ReadonlySet<string>
readonly id: string
readonly name: string
readonly providerMetadataKey: string
readonly routeID: string
readonly tools: ToolStream.State<string>
readonly hasFunctionCall: boolean
readonly lifecycle: Lifecycle.State
@@ -476,15 +476,15 @@ export const lowerToolChoice = (protocolName: string, toolChoice: NonNullable<LL
// Server-issued item ids need a nonempty prefix and suffix, but the prefix is
// provider-defined and does not necessarily identify the item's semantic type.
const itemID = (providerMetadata: ProviderMetadata | undefined, providerMetadataKey: string) => {
const metadata = providerMetadata?.[providerMetadataKey]
const itemID = (providerMetadata: ProviderMetadata | undefined, routeID: string) => {
const metadata = providerMetadata?.[routeID]
if (!ProviderShared.isRecord(metadata) || typeof metadata.itemId !== "string") return undefined
const separator = metadata.itemId.indexOf("_")
return separator > 0 && separator < metadata.itemId.length - 1 ? metadata.itemId : undefined
}
const lowerToolCall = (part: ToolCallPart, providerMetadataKey: string): OpenResponsesInputItem => {
const id = itemID(part.providerMetadata, providerMetadataKey)
const lowerToolCall = (part: ToolCallPart, routeID: string): OpenResponsesInputItem => {
const id = itemID(part.providerMetadata, routeID)
return {
type: "function_call",
...(id === undefined ? {} : { id }),
@@ -495,10 +495,10 @@ const lowerToolCall = (part: ToolCallPart, providerMetadataKey: string): OpenRes
}
}
const lowerReasoning = (part: ReasoningPart, providerMetadataKey: string): OpenResponsesReasoningInput | undefined => {
const metadata = part.providerMetadata?.[providerMetadataKey]
const lowerReasoning = (part: ReasoningPart, routeID: string): OpenResponsesReasoningInput | undefined => {
const metadata = part.providerMetadata?.[routeID]
if (!ProviderShared.isRecord(metadata)) return undefined
const id = itemID(part.providerMetadata, providerMetadataKey)
const id = itemID(part.providerMetadata, routeID)
const encryptedContent =
typeof metadata.reasoningEncryptedContent === "string" || metadata.reasoningEncryptedContent === null
? metadata.reasoningEncryptedContent
@@ -521,7 +521,7 @@ const lowerMedia = Effect.fn("OpenResponses.lowerMedia")(function* (
const providerMedia = adapter.lowerMedia?.({ part, media, request })
if (providerMedia) return providerMedia
const detail = yield* ProviderShared.validateWith(Schema.decodeUnknownEffect(OpenResponsesInputImage.fields.detail))(
part.providerMetadata?.[metadataKey(request.model)]?.detail,
part.providerMetadata?.[request.model.route.id]?.detail,
)
const url =
typeof part.data === "string" && (part.data.startsWith("https://") || part.data.startsWith("http://"))
@@ -608,12 +608,12 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (
adapter: ProviderAdapter,
) {
const input: LoweredInputItem[] = []
const providerMetadataKey = metadataKey(request.model)
const routeID = request.model.route.id
for (const message of request.messages) {
const metadata = yield* ProviderShared.validateWith(
Schema.decodeUnknownEffect(Schema.UndefinedOr(MessageMetadata)),
)(message.providerMetadata?.[providerMetadataKey])
)(message.providerMetadata?.[routeID])
if (message.role === "system") {
input.push({
role: "developer",
@@ -638,8 +638,8 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (
const groups = content.reduce<
Array<{ id: string | undefined; phase: MessagePhase | null | undefined; parts: TextPart[] }>
>((groups, part) => {
const partMetadata = part.providerMetadata?.[providerMetadataKey]
const id = itemID(part.providerMetadata, providerMetadataKey) ?? metadata?.itemId
const partMetadata = part.providerMetadata?.[routeID]
const id = itemID(part.providerMetadata, routeID) ?? metadata?.itemId
const partPhase = messagePhase(partMetadata?.phase)
const phase = partPhase === undefined ? metadata?.phase : partPhase
const group = groups.at(-1)
@@ -675,7 +675,7 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (
}
if (part.type === "reasoning") {
flushText()
const reasoning = lowerReasoning(part, providerMetadataKey)
const reasoning = lowerReasoning(part, routeID)
if (!reasoning) continue
const existing = reasoning.id === undefined ? undefined : reasoningItems[reasoning.id]
if (existing) {
@@ -691,12 +691,12 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (
if (part.type === "tool-call") {
flushText()
if (part.providerExecuted === true) continue
input.push(lowerToolCall(part, providerMetadataKey))
input.push(lowerToolCall(part, routeID))
continue
}
if (part.type === "tool-result" && part.providerExecuted === true) {
flushText()
const id = itemID(part.providerMetadata, providerMetadataKey)
const id = itemID(part.providerMetadata, routeID)
const hosted =
part.result.type !== "json"
? undefined
@@ -844,7 +844,7 @@ export const fromRequest = Effect.fn("OpenResponses.fromRequest")(function* (req
// cached-read and cache-write subsets, and `output_tokens` (inclusive total)
// with a `reasoning_tokens` subset. Pass the totals through and derive the
// non-cached breakdown.
export const mapUsage = (usage: OpenResponsesUsage | null | undefined, providerMetadataKey: string) => {
export const mapUsage = (usage: OpenResponsesUsage | null | undefined, routeID: string) => {
if (!usage) return undefined
const cached = usage.input_tokens_details?.cached_tokens
const cacheWrite = usage.input_tokens_details?.cache_write_tokens
@@ -858,7 +858,7 @@ export const mapUsage = (usage: OpenResponsesUsage | null | undefined, providerM
cacheWriteInputTokens: cacheWrite,
reasoningTokens: reasoning,
totalTokens: ProviderShared.totalTokens(usage.input_tokens, usage.output_tokens, usage.total_tokens),
providerMetadata: { [providerMetadataKey]: usage },
providerMetadata: { [routeID]: usage },
})
}
@@ -874,10 +874,8 @@ const mapFinishReason = (event: Event, hasFunctionCall: boolean): FinishReason =
return hasFunctionCall ? "tool-calls" : "unknown"
}
export const metadataKey = (model: LLMRequest["model"]) => model.route.providerMetadataKey ?? "openresponses"
export const providerMetadata = (state: ParserState, metadata: Record<string, unknown>): ProviderMetadata => ({
[state.providerMetadataKey]: metadata,
[state.routeID]: metadata,
})
export type StepResult = readonly [ParserState, ReadonlyArray<LLMEvent>]
@@ -1349,7 +1347,7 @@ const onResponseFinish = Effect.fn("OpenResponses.onResponseFinish")(function* (
normalized: mapFinishReason(event, current.hasFunctionCall),
raw: event.response?.incomplete_details?.reason,
},
usage: mapUsage(event.response?.usage, current.providerMetadataKey),
usage: mapUsage(event.response?.usage, current.routeID),
providerMetadata:
event.response?.id || event.response?.service_tier
? providerMetadata(current, {
@@ -1486,7 +1484,7 @@ export const initial = (request: LLMRequest, adapter: ProviderAdapter = BASE_ADA
completedCompactions: new Set<string>(),
id: adapter.id,
name: adapter.name,
providerMetadataKey: metadataKey(request.model),
routeID: request.model.route.id,
hasFunctionCall: false,
tools: ToolStream.empty<string>(),
lifecycle: Lifecycle.initial(),
+23 -25
View File
@@ -257,7 +257,7 @@ interface PendingToolDelta {
}
export interface ParserState {
readonly providerMetadataKey: string
readonly routeID: string
readonly tools: ToolStream.State<number>
readonly pendingTools: Partial<Record<number, PendingToolDelta>>
readonly toolCallEvents: ReadonlyArray<LLMEvent>
@@ -329,18 +329,17 @@ const lowerMedia = Effect.fn("OpenAIChat.lowerMedia")(function* (part: MediaPart
const openAICompatibleReasoningContent = (native: unknown) =>
isRecord(native) && typeof native.reasoning_content === "string" ? native.reasoning_content : undefined
const reasoningField = (part: ReasoningPart, providerMetadataKey: string) => {
const field = part.providerMetadata?.[providerMetadataKey]?.reasoningField
const reasoningField = (part: ReasoningPart, routeID: string) => {
const field = part.providerMetadata?.[routeID]?.reasoningField
return typeof field === "string" ? field : undefined
}
const reasoningDetails = (parts: ReadonlyArray<ReasoningPart>, native: unknown, providerMetadataKey: string) => {
const reasoningDetails = (parts: ReadonlyArray<ReasoningPart>, native: unknown, routeID: string) => {
const observed = parts.flatMap((part) => {
const details = part.providerMetadata?.[providerMetadataKey]?.reasoningDetails
const details = part.providerMetadata?.[routeID]?.reasoningDetails
return Array.isArray(details) ? details : []
})
if (parts.some((part) => Array.isArray(part.providerMetadata?.[providerMetadataKey]?.reasoningDetails)))
return observed
if (parts.some((part) => Array.isArray(part.providerMetadata?.[routeID]?.reasoningDetails))) return observed
if (isRecord(native) && Array.isArray(native.reasoning_details)) return native.reasoning_details
}
@@ -372,7 +371,7 @@ const lowerAssistantMessage = Effect.fn("OpenAIChat.lowerAssistantMessage")(func
message: OpenAIChatRequestMessage,
configuredField: string | undefined,
requireReasoning: boolean,
options: LoweringOptions & { readonly providerMetadataKey: string },
options: LoweringOptions & { readonly routeID: string },
) {
const content: TextPart[] = []
const reasoning: ReasoningPart[] = []
@@ -394,13 +393,13 @@ const lowerAssistantMessage = Effect.fn("OpenAIChat.lowerAssistantMessage")(func
}
}
const text = reasoning.map((part) => part.text).join("")
const details = reasoningDetails(reasoning, message.native?.openaiCompatible, options.providerMetadataKey)
const details = reasoningDetails(reasoning, message.native?.openaiCompatible, options.routeID)
const observedField = reasoning
.map((part) => reasoningField(part, options.providerMetadataKey))
.map((part) => reasoningField(part, options.routeID))
.find((value) => value !== undefined)
const nativeReasoning = openAICompatibleReasoningContent(message.native?.openaiCompatible)
const fullyStructured = reasoning.every((part) =>
Array.isArray(part.providerMetadata?.[options.providerMetadataKey]?.reasoningDetails),
Array.isArray(part.providerMetadata?.[options.routeID]?.reasoningDetails),
)
const field = (() => {
if (configuredField !== undefined && (requireReasoning || reasoning.length > 0 || nativeReasoning !== undefined))
@@ -469,7 +468,7 @@ const lowerMessage = Effect.fn("OpenAIChat.lowerMessage")(function* (
message: OpenAIChatRequestMessage,
reasoningField: string | undefined,
requireReasoning: boolean,
options: LoweringOptions & { readonly providerMetadataKey: string },
options: LoweringOptions & { readonly routeID: string },
) {
if (message.role === "user") return [yield* lowerUserMessage(message, options)]
if (message.role === "assistant")
@@ -505,7 +504,7 @@ const lowerMessages = Effect.fn("OpenAIChat.lowerMessages")(function* (request:
const mistral = ["mistral", "devstral", "codestral", "pixtral", "mixtral"].some((family) => modelID.includes(family))
const lowering = {
...options,
providerMetadataKey: request.model.route.providerMetadataKey ?? String(request.model.provider),
routeID: request.model.route.id,
toolCallID: (id: string) => {
if (mistral)
return id
@@ -833,7 +832,7 @@ const mapFinishReason = Effect.fn("OpenAIChat.mapFinishReason")(function* (event
// Providers differ on cache-hit location: OpenAI uses
// `prompt_tokens_details.cached_tokens`, DeepSeek uses
// `prompt_cache_hit_tokens`, and Zai uses top-level `cached_tokens`.
const mapUsage = (usage: OpenAIChatEvent["usage"], providerMetadataKey: string): Usage | undefined => {
const mapUsage = (usage: OpenAIChatEvent["usage"], routeID: string): Usage | undefined => {
if (!usage) return undefined
const input = usage.prompt_tokens ?? undefined
const output = usage.completion_tokens ?? undefined
@@ -852,7 +851,7 @@ const mapUsage = (usage: OpenAIChatEvent["usage"], providerMetadataKey: string):
cacheWriteInputTokens: cacheWrite,
reasoningTokens: reasoning,
totalTokens: ProviderShared.totalTokens(input, output, usage.total_tokens ?? undefined),
providerMetadata: { [providerMetadataKey]: usage },
providerMetadata: { [routeID]: usage },
})
}
@@ -927,11 +926,11 @@ const conflictingDetailValue = (previous: unknown, current: unknown) =>
previous !== undefined && previous !== null && current !== undefined && current !== null && previous !== current
const reasoningMetadata = (
providerMetadataKey: string,
routeID: string,
field: ParserState["reasoningField"],
details?: ReadonlyArray<unknown>,
) => ({
[providerMetadataKey]: {
[routeID]: {
...(field ? { reasoningField: field } : {}),
...(details ? { reasoningDetails: details } : {}),
},
@@ -955,8 +954,8 @@ const step = (state: ParserState, event: OpenAIChatEvent) =>
// `choice.usage` instead of the top-level `usage` field.
const choiceUsage = (choice as unknown as { usage?: OpenAIChatEvent["usage"] })?.usage
const usage =
mapUsage(event.usage, state.providerMetadataKey) ??
(choiceUsage ? mapUsage(choiceUsage, state.providerMetadataKey) : undefined) ??
mapUsage(event.usage, state.routeID) ??
(choiceUsage ? mapUsage(choiceUsage, state.routeID) : undefined) ??
state.usage
const rawFinishReason = choice?.finish_reason
const finishReason = rawFinishReason
@@ -995,7 +994,7 @@ const step = (state: ParserState, event: OpenAIChatEvent) =>
const detailDelta = Array.isArray(delta?.reasoning_details) ? delta.reasoning_details : undefined
if (detailDelta !== undefined) appendReasoningDetails(state.reasoningDetails, detailDelta)
const reasoningDetailsObserved = state.reasoningDetailsObserved || detailDelta !== undefined
const deltaMetadata = reasoningMetadata(state.providerMetadataKey, reasoningField)
const deltaMetadata = reasoningMetadata(state.routeID, reasoningField)
const text = detailDelta?.length ? (detailText(detailDelta) ?? reasoning?.text) : reasoning?.text
if (text !== undefined) lifecycle = Lifecycle.reasoningDelta(lifecycle, events, "reasoning-0", text, deltaMetadata)
else if (
@@ -1084,7 +1083,7 @@ const step = (state: ParserState, event: OpenAIChatEvent) =>
return [
{
providerMetadataKey: state.providerMetadataKey,
routeID: state.routeID,
tools: finished?.tools ?? tools,
pendingTools,
toolCallEvents: finished?.events ?? state.toolCallEvents,
@@ -1128,7 +1127,7 @@ const finishEvents = Effect.fn("OpenAIChat.finishEvents")(function* (state: Pars
// Snapshot details at publish time so the emitted event never observes later
// mutation of the accumulated `reasoningDetails` array.
const metadata = reasoningMetadata(
state.providerMetadataKey,
state.routeID,
state.reasoningField,
state.reasoningDetailsObserved ? [...state.reasoningDetails] : undefined,
)
@@ -1138,7 +1137,7 @@ const finishEvents = Effect.fn("OpenAIChat.finishEvents")(function* (state: Pars
state.lifecycle,
events,
"reasoning-0",
reasoningMetadata(state.providerMetadataKey, state.reasoningField),
reasoningMetadata(state.routeID, state.reasoningField),
)
: state.lifecycle
const ended = Lifecycle.reasoningEnd(started, events, "reasoning-0", metadata)
@@ -1166,7 +1165,7 @@ export const protocol = Protocol.make({
stream: {
event: OpenAIChatStreamEvent,
initial: (request) => ({
providerMetadataKey: request.model.route.providerMetadataKey ?? String(request.model.provider),
routeID: request.model.route.id,
tools: ToolStream.empty<number>(),
pendingTools: {},
toolCallEvents: [],
@@ -1190,7 +1189,6 @@ export const httpTransport = HttpTransport.sseJson.with<OpenAIChatBody>().with({
export const route = Route.make({
id: ADAPTER,
provider: "openai",
providerMetadataKey: "openai",
protocol,
endpoint: Endpoint.path(PATH, { baseURL: DEFAULT_BASE_URL }),
auth: Auth.none,
@@ -15,7 +15,6 @@ export type OpenAICompatibleChatLanguageModelInput = RouteRoutedLanguageModelInp
*/
export const route = Route.make({
id: ADAPTER,
providerMetadataKey: "openai",
protocol: OpenAIChat.protocol,
endpoint: Endpoint.path("/chat/completions"),
framing: OpenAIChat.framing,
@@ -13,7 +13,6 @@ export type OpenAICompatibleResponsesLanguageModelInput = RouteRoutedLanguageMod
*/
export const route = Route.make({
id: ADAPTER,
providerMetadataKey: "openresponses",
protocol: OpenResponses.protocol,
endpoint: Endpoint.path(OpenResponses.PATH),
transport: OpenResponses.httpTransport,
@@ -313,7 +313,6 @@ export const route = Route.make({
compact: { endpoint: ResponsesCompaction.make(adapter), trigger: ResponsesCheckpoint.make(checkpointBody) },
id: ADAPTER,
provider: "openai",
providerMetadataKey: "openai",
protocol,
endpoint,
auth,
@@ -93,7 +93,7 @@ export const make = <Body>(body: RouteBody<Body>): TriggerCompactOperation =>
result = new CompactionCheckpointResponse({
checkpoint,
responseID,
usage: OpenResponses.mapUsage(event.response?.usage, OpenResponses.metadataKey(request.model)),
usage: OpenResponses.mapUsage(event.response?.usage, request.model.route.id),
})
return [next, [LLMEvent.finish({ reason: { normalized: "stop" } })]] as const
}),
@@ -102,7 +102,6 @@ export const make = <Body>(body: RouteBody<Body>): TriggerCompactOperation =>
const route = Route.make({
id: source.id,
provider: source.provider,
providerMetadataKey: source.providerMetadataKey,
protocol,
endpoint: source.endpoint,
auth: source.auth,
@@ -125,7 +125,7 @@ export const make = (adapter: OpenResponses.ProviderAdapter): CompactOperation =
return yield* invalid("Compaction response did not contain a checkpoint")
return new CompactionResponse({
replacement: result.output.map((item) => toMessage(item, request.model)),
usage: OpenResponses.mapUsage(result.usage, OpenResponses.metadataKey(request.model)),
usage: OpenResponses.mapUsage(result.usage, request.model.route.id),
})
})
@@ -135,7 +135,7 @@ function toMessage(item: (typeof Response.Type.output)[number], model: LLMReques
CompactionPart.make({ provider: model.provider, id: item.id ?? undefined, encrypted: item.encrypted_content }),
)
const key = OpenResponses.metadataKey(model)
const key = model.route.id
if (item.type === "reasoning") {
const summary = item.summary.length ? item.summary : [{ text: "" }]
return Message.assistant(
@@ -36,7 +36,6 @@ export interface Settings extends ProviderPackage.Settings {
const responsesRoute = Route.make({
id: "bedrock-mantle-responses",
provider: id,
providerMetadataKey: "mantle",
protocol: OpenAIResponses.protocol,
endpoint: OpenAIResponses.route.endpoint,
auth: OpenAIResponses.route.auth,
@@ -47,7 +46,6 @@ const responsesRoute = Route.make({
const chatRoute = OpenAIChat.route.with({
id: "bedrock-mantle-chat",
provider: id,
providerMetadataKey: "mantle",
})
export const routes = [responsesRoute, chatRoute]
@@ -45,7 +45,6 @@ const configuredRoute = (input: Config) => {
return BedrockConverse.route.with({
...rest,
provider: id,
providerMetadataKey: "bedrock",
endpoint: { baseURL: baseURL ?? bedrockBaseURL(resolvedRegion) },
auth: BedrockAuth.resolveAuth({ apiKey, credentials, profile }, resolvedRegion, { mode: auth }),
})
-1
View File
@@ -24,7 +24,6 @@ export interface Settings extends ProviderPackage.Settings {
export const route = Route.make({
id: "baseten-chat",
provider: id,
providerMetadataKey: "baseten",
protocol: OpenAIChat.protocol,
endpoint: Endpoint.path("/chat/completions", { baseURL }),
framing: OpenAIChat.framing,
-1
View File
@@ -24,7 +24,6 @@ export interface Settings extends ProviderPackage.Settings {
export const route = Route.make({
id: "cerebras-chat",
provider: id,
providerMetadataKey: "cerebras",
protocol: OpenAIChat.protocol,
endpoint: Endpoint.path("/chat/completions", { baseURL }),
framing: OpenAIChat.framing,
@@ -53,7 +53,6 @@ const auth = (input: LanguageModelOptions) => {
export const route = Route.make({
id: "cloudflare-ai-gateway",
provider: id,
providerMetadataKey: "cloudflare-ai-gateway",
protocol: OpenAIChat.protocol,
endpoint: Endpoint.path("/chat/completions"),
framing: OpenAIChat.framing,
@@ -35,7 +35,6 @@ export const baseURL = (input: WorkersAIURL) => {
export const route = Route.make({
id: "cloudflare-workers-ai",
provider: id,
providerMetadataKey: "cloudflare-workers-ai",
protocol: OpenAIChat.protocol,
endpoint: Endpoint.path("/chat/completions"),
framing: OpenAIChat.framing,
-1
View File
@@ -24,7 +24,6 @@ export interface Settings extends ProviderPackage.Settings {
export const route = Route.make({
id: "deepinfra-chat",
provider: id,
providerMetadataKey: "deepinfra",
protocol: OpenAIChat.protocol,
endpoint: Endpoint.path("/chat/completions", { baseURL }),
framing: OpenAIChat.framing,
-1
View File
@@ -24,7 +24,6 @@ export interface Settings extends ProviderPackage.Settings {
export const route = Route.make({
id: "deepseek-chat",
provider: id,
providerMetadataKey: "deepseek",
protocol: OpenAIChat.protocol,
endpoint: Endpoint.path("/chat/completions", { baseURL }),
framing: OpenAIChat.framing,
-1
View File
@@ -24,7 +24,6 @@ export interface Settings extends ProviderPackage.Settings {
export const route = Route.make({
id: "fireworks-chat",
provider: id,
providerMetadataKey: "fireworks",
protocol: OpenAIChat.protocol,
endpoint: Endpoint.path("/chat/completions", { baseURL }),
framing: OpenAIChat.framing,
@@ -28,7 +28,6 @@ export interface Settings extends ProviderPackage.Settings {
const route = Route.make({
id: "google-vertex-chat",
provider: id,
providerMetadataKey: "vertex",
protocol: OpenAIChat.protocol,
endpoint: Endpoint.path("/chat/completions"),
framing: OpenAIChat.framing,
@@ -37,7 +37,6 @@ export interface Settings extends ProviderPackage.Settings {
const route = Route.make({
id: "google-vertex-messages",
provider: id,
providerMetadataKey: "anthropic",
protocol: Protocol.make({
id: AnthropicMessages.protocol.id,
body: {
@@ -28,7 +28,6 @@ export interface Settings extends ProviderPackage.Settings {
const route = Route.make({
id: "google-vertex-responses",
provider: id,
providerMetadataKey: "vertex",
protocol: OpenResponses.protocol,
endpoint: Endpoint.path(OpenResponses.PATH),
transport: OpenResponses.httpTransport,
@@ -68,7 +68,6 @@ const protocol = {
const route = Route.make({
id: "google-vertex-gemini",
provider: id,
providerMetadataKey: "vertex",
protocol,
endpoint: Endpoint.path(({ request }) => {
const model = String(request.model.id)
-1
View File
@@ -71,7 +71,6 @@ export const protocol = Protocol.make({
export const route = Route.make({
id: "groq-chat",
provider: id,
providerMetadataKey: "openai",
protocol,
endpoint: Endpoint.path("/chat/completions", { baseURL }),
framing: OpenAIChat.framing,
-1
View File
@@ -162,7 +162,6 @@ const bodyOptions = (input: unknown) => {
export const route = Route.make({
id: ADAPTER,
provider: id,
providerMetadataKey: "openrouter",
protocol,
endpoint: Endpoint.path("/chat/completions", { baseURL }),
framing: OpenAIChat.framing,
-1
View File
@@ -24,7 +24,6 @@ export interface Settings extends ProviderPackage.Settings {
export const route = Route.make({
id: "togetherai-chat",
provider: id,
providerMetadataKey: "togetherai",
protocol: OpenAIChat.protocol,
endpoint: Endpoint.path("/chat/completions", { baseURL }),
framing: OpenAIChat.framing,
+3 -5
View File
@@ -32,13 +32,12 @@ const RESPONSES_WEBSOCKET_ROTATE_AFTER_MS = 24 * 60 * 1000
const responsesRoute = Route.make({
compact: { endpoint: XAIResponses.compact },
id: "openai-responses",
id: "xai-responses",
provider: id,
providerMetadataKey: "xai",
protocol: XAIResponses.protocol,
endpoint: Endpoint.path("/responses", { baseURL }),
transport: OpenResponsesChannel.transport({
id: "openai-responses",
id: "xai-responses",
name: "xAI Responses",
rotateAfterMs: RESPONSES_WEBSOCKET_ROTATE_AFTER_MS,
}),
@@ -46,9 +45,8 @@ const responsesRoute = Route.make({
})
const chatRoute = Route.make({
id: "openai-compatible-chat",
id: "xai-chat",
provider: id,
providerMetadataKey: "xai",
protocol: OpenAIChat.protocol,
endpoint: Endpoint.path("/chat/completions", { baseURL }),
framing: OpenAIChat.framing,
+4 -17
View File
@@ -42,10 +42,9 @@ export interface Route<
Compact extends CompactionOperations | undefined = CompactionOperations | undefined,
> {
readonly compact: Compact
/** Route identity and namespace for metadata emitted and replayed by this route. */
readonly id: string
readonly provider?: ProviderID
/** ProviderMetadata namespace emitted and consumed by this route. */
readonly providerMetadataKey?: string
readonly protocol: ProtocolID
readonly endpoint: Endpoint.Definition<Body>
readonly auth: Auth.Definition
@@ -113,7 +112,6 @@ export interface RoutePatch<Body, Prepared> extends RouteDefaultsInput {
readonly compact?: CompactionOperations
readonly id?: string
readonly provider?: string | ProviderID
readonly providerMetadataKey?: string
readonly auth?: Auth.Definition
readonly transport?: Transport<Body, Prepared, unknown>
readonly endpoint?: EndpointPatch<Body>
@@ -276,12 +274,10 @@ const resolveRequestOptions = (request: LLMRequest) => {
export interface MakeInput<Body, Frame, Event, State> {
readonly compact?: CompactionOperations
/** Route id used in diagnostics and prepared request metadata. */
/** Route identity, also used as the namespace for emitted and replayed ProviderMetadata. */
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<Body, Frame, Event, State>
/** Where the request is sent. */
@@ -298,12 +294,10 @@ export interface MakeInput<Body, Frame, Event, State> {
export interface MakeTransportInput<Body, Prepared, Frame, Event, State> {
readonly compact?: CompactionOperations
/** Route id used in diagnostics and prepared request metadata. */
/** Route identity, also used as the namespace for emitted and replayed ProviderMetadata. */
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<Body, Frame, Event, State>
/** Where the request is sent. */
@@ -376,7 +370,6 @@ function makeFromTransport<Body, Prepared, Frame, Event, State>(
compact: routeInput.compact,
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,
@@ -385,17 +378,12 @@ function makeFromTransport<Body, Prepared, Frame, Event, State>(
defaults: routeInput.defaults ?? {},
body: protocol.body,
with: (patch: RoutePatch<Body, Prepared>) => {
const { compact, id, provider, providerMetadataKey, auth, transport, endpoint, ...defaults } = patch
const { compact, id, provider, auth, transport, endpoint, ...defaults } = patch
return build({
...routeInput,
compact: "compact" in patch ? compact : routeInput.compact,
id: id ?? routeInput.id,
provider: provider ?? routeInput.provider,
providerMetadataKey:
providerMetadataKey ??
(provider !== undefined && String(provider) !== String(routeInput.provider)
? String(provider)
: routeInput.providerMetadataKey),
auth: auth ?? routeInput.auth,
endpoint: endpoint ? Endpoint.merge(routeInput.endpoint, endpoint) : routeInput.endpoint,
transport: (transport as Transport<Body, Prepared, Frame> | undefined) ?? routeInput.transport,
@@ -536,7 +524,6 @@ export function make<Body, Prepared, Frame, Event, State>(
compact: input.compact,
id: input.id,
provider: input.provider,
providerMetadataKey: input.providerMetadataKey,
protocol,
endpoint: input.endpoint,
auth: input.auth,
+1
View File
@@ -15,6 +15,7 @@ import { ProviderID } from "./ids.js"
export const MessageRole = Schema.Literals(["system", "user", "assistant", "tool"])
export type MessageRole = Schema.Schema.Type<typeof MessageRole>
/** Provider-specific data; LLM routes emit and replay it under their route ID. */
export const ProviderMetadata = Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.Unknown)).annotate({
identifier: "LLM.ProviderMetadata",
})
+2 -2
View File
@@ -66,14 +66,14 @@ export function continuationRequest(input: {
firstAssistant.push({
type: "reasoning",
text: "I inspected the previous turn.",
providerMetadata: { anthropic: { signature: "sig_continuation_1" } },
providerMetadata: { [input.model.route.id]: { signature: "sig_continuation_1" } },
})
if (features.has("encrypted-reasoning"))
firstAssistant.push({
type: "reasoning",
text: "I inspected the previous turn.",
providerMetadata: {
openai: {
[input.model.route.id]: {
itemId: "rs_continuation_1",
reasoningEncryptedContent: "encrypted-continuation-state",
},
+2 -2
View File
@@ -69,8 +69,8 @@ describe("public exports", () => {
expect(XAI.model).toBeFunction()
expect(XAI.provider.responses).toBe(XAI.responses)
expect(XAI.provider.chat).toBe(XAI.chat)
expect(XAI.configure({ apiKey: "fixture" }).responses("grok-4.3").route.id).toBe("openai-responses")
expect(XAI.configure({ apiKey: "fixture" }).chat("grok-4.3").route.id).toBe("openai-compatible-chat")
expect(XAI.configure({ apiKey: "fixture" }).responses("grok-4.3").route.id).toBe("xai-responses")
expect(XAI.configure({ apiKey: "fixture" }).chat("grok-4.3").route.id).toBe("xai-chat")
})
test("protocol barrels expose supported low-level routes", () => {
@@ -102,7 +102,7 @@ describe("Anthropic Messages route", () => {
Message.assistant([{ type: "reasoning", text: "" }]),
Message.assistant([
{ type: "text", text: "" },
{ type: "reasoning", text: "", providerMetadata: { anthropic: { signature: "sig_1" } } },
{ type: "reasoning", text: "", providerMetadata: { "anthropic-messages": { signature: "sig_1" } } },
]),
],
cache: "none",
@@ -753,7 +753,11 @@ describe("Anthropic Messages route", () => {
model,
messages: [
Message.assistant([
{ type: "reasoning", text: "thinking", providerMetadata: { anthropic: { signature: "sig_1" } } },
{
type: "reasoning",
text: "thinking",
providerMetadata: { "anthropic-messages": { signature: "sig_1" } },
},
]),
],
}),
@@ -831,8 +835,12 @@ describe("Anthropic Messages route", () => {
model,
messages: [
Message.assistant([
{ type: "reasoning", text: "", providerMetadata: { anthropic: { redactedData: "opaque_1" } } },
{ type: "reasoning", text: "visible", providerMetadata: { anthropic: { signature: "sig_1" } } },
{ type: "reasoning", text: "", providerMetadata: { "anthropic-messages": { redactedData: "opaque_1" } } },
{
type: "reasoning",
text: "visible",
providerMetadata: { "anthropic-messages": { signature: "sig_1" } },
},
]),
],
}),
@@ -912,20 +920,28 @@ describe("Anthropic Messages route", () => {
)
expect(response.message.content).toMatchObject([
{ type: "reasoning", text: "Thinking.", providerMetadata: { "custom-anthropic": { signature: "custom_sig" } } },
{ type: "reasoning", text: "", providerMetadata: { "custom-anthropic": { redactedData: "custom_redacted" } } },
{
type: "reasoning",
text: "Thinking.",
providerMetadata: { "custom-anthropic-messages": { signature: "custom_sig" } },
},
{
type: "reasoning",
text: "",
providerMetadata: { "custom-anthropic-messages": { redactedData: "custom_redacted" } },
},
{ type: "tool-call", id: "custom_tool", providerExecuted: true },
{
type: "tool-result",
providerExecuted: true,
providerMetadata: { "custom-anthropic": { blockType: "web_search_tool_result", result } },
providerMetadata: { "custom-anthropic-messages": { blockType: "web_search_tool_result", result } },
},
])
expect(response.usage?.providerMetadata).toEqual({
"custom-anthropic": { input_tokens: 5, custom_start: true, output_tokens: 2, custom_terminal: true },
"custom-anthropic-messages": { input_tokens: 5, custom_start: true, output_tokens: 2, custom_terminal: true },
})
expect(response.events.at(-1)).toMatchObject({
providerMetadata: { "custom-anthropic": { stopSequence: "custom_stop" } },
providerMetadata: { "custom-anthropic-messages": { stopSequence: "custom_stop" } },
})
const prepared = yield* compileRequest(
@@ -974,13 +990,13 @@ describe("Anthropic Messages route", () => {
const reasoningEnds = response.events.filter((event) => event.type === "reasoning-end")
expect(reasoningEnds).toHaveLength(1)
expect(reasoningEnds[0]).toMatchObject({
providerMetadata: { "custom-anthropic": { signature: "sig_1" } },
providerMetadata: { "custom-anthropic-messages": { signature: "sig_1" } },
})
expect(response.message.content).toEqual([
{
type: "reasoning",
text: "Reasoning.",
providerMetadata: { "custom-anthropic": { signature: "sig_1" } },
providerMetadata: { "custom-anthropic-messages": { signature: "sig_1" } },
},
])
@@ -1024,18 +1040,18 @@ describe("Anthropic Messages route", () => {
totalTokens: 8,
})
expect(response.events.find((event) => event.type === "reasoning-end")).toMatchObject({
providerMetadata: { anthropic: { signature: "sig_1" } },
providerMetadata: { "anthropic-messages": { signature: "sig_1" } },
})
expect(response.events.filter((event) => event.type === "reasoning-end")).toHaveLength(1)
expect(response.events.find((event) => event.type === "reasoning-delta" && event.text === "")).toBeUndefined()
expect(response.message.content).toEqual([
{ type: "text", text: "Hello!" },
{ type: "reasoning", text: "thinking", providerMetadata: { anthropic: { signature: "sig_1" } } },
{ type: "reasoning", text: "thinking", providerMetadata: { "anthropic-messages": { signature: "sig_1" } } },
])
expect(response.events.at(-1)).toMatchObject({
type: "finish",
reason: { normalized: "stop", raw: "end_turn" },
providerMetadata: { anthropic: { stopSequence: "\n\nHuman:" } },
providerMetadata: { "anthropic-messages": { stopSequence: "\n\nHuman:" } },
})
}),
)
@@ -1064,13 +1080,13 @@ describe("Anthropic Messages route", () => {
expect(response.events.find((event) => event.type === "step-finish")).toMatchObject({
reason: { normalized: "stop", raw: "end_turn" },
usage: { inputTokens: 5, outputTokens: 10, totalTokens: 15 },
providerMetadata: { anthropic: { stopSequence: "X" } },
providerMetadata: { "anthropic-messages": { stopSequence: "X" } },
})
expect(response.events.at(-1)).toMatchObject({
type: "finish",
reason: { normalized: "stop", raw: "end_turn" },
usage: { inputTokens: 5, outputTokens: 10, totalTokens: 15 },
providerMetadata: { anthropic: { stopSequence: "X" } },
providerMetadata: { "anthropic-messages": { stopSequence: "X" } },
})
}),
)
@@ -1296,7 +1312,7 @@ describe("Anthropic Messages route", () => {
reasoningTokens: 3,
totalTokens: 15,
providerMetadata: {
anthropic: {
"anthropic-messages": {
input_tokens: null,
cache_read_input_tokens: 2,
service_tier: "standard",
@@ -1337,7 +1353,7 @@ describe("Anthropic Messages route", () => {
)
expect(response.message.content).toEqual([
{ type: "reasoning", text: "", providerMetadata: { anthropic: { signature: "sig_1" } } },
{ type: "reasoning", text: "", providerMetadata: { "anthropic-messages": { signature: "sig_1" } } },
])
const prepared = yield* compileRequest(LLM.request({ model, messages: [response.message], cache: "none" }))
@@ -1368,10 +1384,10 @@ describe("Anthropic Messages route", () => {
)
expect(response.message.content).toEqual([
{ type: "reasoning", text: "", providerMetadata: { anthropic: { signature: "sig_1" } } },
{ type: "reasoning", text: "", providerMetadata: { "anthropic-messages": { signature: "sig_1" } } },
])
expect(response.events.find((event) => event.type === "reasoning-end")).toMatchObject({
providerMetadata: { anthropic: { signature: "sig_1" } },
providerMetadata: { "anthropic-messages": { signature: "sig_1" } },
})
}),
)
@@ -1435,10 +1451,10 @@ describe("Anthropic Messages route", () => {
const response = yield* LLMClient.generate(request).pipe(Effect.provide(fixedResponse(body)))
expect(response.events.find((event) => event.type === "reasoning-start")).toMatchObject({
providerMetadata: { anthropic: { redactedData: "opaque_1" } },
providerMetadata: { "anthropic-messages": { redactedData: "opaque_1" } },
})
expect(response.message.content).toEqual([
{ type: "reasoning", text: "", providerMetadata: { anthropic: { redactedData: "opaque_1" } } },
{ type: "reasoning", text: "", providerMetadata: { "anthropic-messages": { redactedData: "opaque_1" } } },
{ type: "text", text: "Hello" },
])
}),
@@ -1582,7 +1598,7 @@ describe("Anthropic Messages route", () => {
cacheReadInputTokens: undefined,
cacheWriteInputTokens: undefined,
totalTokens: 6,
providerMetadata: { anthropic: { input_tokens: 5, output_tokens: 1 } },
providerMetadata: { "anthropic-messages": { input_tokens: 5, output_tokens: 1 } },
})
expect(response.toolCalls).toEqual([
@@ -1891,7 +1907,7 @@ describe("Anthropic Messages route", () => {
// The complete payload rides in provider metadata as irreducible replay
// state for later stateless requests.
providerMetadata: {
anthropic: {
"anthropic-messages": {
blockType: "web_search_tool_result",
result: [{ type: "web_search_result", url: "https://example.com", title: "Example" }],
},
@@ -837,7 +837,7 @@ describe("Bedrock Converse route", () => {
{
type: "reasoning",
text: "Partial thought",
providerMetadata: signature === undefined ? undefined : { bedrock: { signature } },
providerMetadata: signature === undefined ? undefined : { "bedrock-converse": { signature } },
cache: new CacheHint({ type: "ephemeral" }),
},
]),
@@ -865,7 +865,7 @@ describe("Bedrock Converse route", () => {
Message.user("Think"),
Message.assistant([
{ type: "reasoning", text: "", cache },
{ type: "reasoning", text: " ", providerMetadata: { bedrock: { signature: "" } }, cache },
{ type: "reasoning", text: " ", providerMetadata: { "bedrock-converse": { signature: "" } }, cache },
]),
Message.user([{ type: "text", text: "Continue", cache }]),
],
@@ -893,11 +893,16 @@ describe("Bedrock Converse route", () => {
{
type: "reasoning",
text: "Signed thought",
providerMetadata: { bedrock: { signature: "sig_1" } },
providerMetadata: { "bedrock-converse": { signature: "sig_1" } },
cache,
},
{ type: "reasoning", text: "", encrypted: "sig_2", cache },
{ type: "reasoning", text: "", providerMetadata: { bedrock: { redactedData: "cmVkYWN0ZWQ=" } }, cache },
{
type: "reasoning",
text: "",
providerMetadata: { "bedrock-converse": { redactedData: "cmVkYWN0ZWQ=" } },
cache,
},
{ type: "text", text: "Checking" },
ToolCallPart.make({ id: "call_1", name: "lookup", input: {} }),
]),
@@ -937,7 +942,7 @@ describe("Bedrock Converse route", () => {
expect(reasoning).toEqual({
type: "reasoning-end",
id: "reasoning-0",
providerMetadata: { bedrock: { signature: "sig_1" } },
providerMetadata: { "bedrock-converse": { signature: "sig_1" } },
})
const prepared = yield* compileRequest(
@@ -960,7 +965,7 @@ describe("Bedrock Converse route", () => {
}),
)
it.effect("round-trips reassigned provider reasoning and usage metadata in its own namespace", () =>
it.effect("preserves route-keyed reasoning and usage metadata when the provider is reassigned", () =>
Effect.gen(function* () {
const compatible = model.route.with({ provider: "custom-bedrock" }).model({ id: model.id })
const redactedData = "cmVkYWN0ZWQtdGhpbmtpbmc="
@@ -988,12 +993,12 @@ describe("Bedrock Converse route", () => {
{
type: "reasoning",
text: "Let me think.",
providerMetadata: { "custom-bedrock": { signature: "custom_sig" } },
providerMetadata: { "bedrock-converse": { signature: "custom_sig" } },
},
{ type: "reasoning", text: "", providerMetadata: { "custom-bedrock": { redactedData } } },
{ type: "reasoning", text: "", providerMetadata: { "bedrock-converse": { redactedData } } },
])
expect(response.usage?.providerMetadata).toEqual({
"custom-bedrock": { inputTokens: 5, outputTokens: 2, totalTokens: 7 },
"bedrock-converse": { inputTokens: 5, outputTokens: 2, totalTokens: 7 },
})
const prepared = yield* compileRequest(
@@ -1030,13 +1035,13 @@ describe("Bedrock Converse route", () => {
type: "reasoning-delta",
id: "reasoning-0",
text: "",
providerMetadata: { bedrock: { signature: "sig_1" } },
providerMetadata: { "bedrock-converse": { signature: "sig_1" } },
})
expect(response.message.content).toEqual([
{
type: "reasoning",
text: "Let me think.",
providerMetadata: { bedrock: { signature: "sig_1" } },
providerMetadata: { "bedrock-converse": { signature: "sig_1" } },
},
])
@@ -1061,7 +1066,7 @@ describe("Bedrock Converse route", () => {
const response = yield* LLMClient.generate(baseRequest).pipe(Effect.provide(fixedBytes(body)))
expect(response.message.content).toEqual([
{ type: "reasoning", text: "", providerMetadata: { bedrock: { signature: "sig_1" } } },
{ type: "reasoning", text: "", providerMetadata: { "bedrock-converse": { signature: "sig_1" } } },
])
}),
)
@@ -1081,10 +1086,10 @@ describe("Bedrock Converse route", () => {
type: "reasoning-delta",
id: "reasoning-0",
text: "",
providerMetadata: { bedrock: { redactedData } },
providerMetadata: { "bedrock-converse": { redactedData } },
})
expect(response.message.content).toEqual([
{ type: "reasoning", text: "", providerMetadata: { bedrock: { redactedData } } },
{ type: "reasoning", text: "", providerMetadata: { "bedrock-converse": { redactedData } } },
])
}),
)
@@ -1124,12 +1129,12 @@ describe("Bedrock Converse route", () => {
type: "reasoning-delta",
id: "reasoning-0",
text: "",
providerMetadata: { bedrock: { redactedData } },
providerMetadata: { "bedrock-converse": { redactedData } },
})
expect(response.events.find((event) => event.type === "reasoning-end")).toEqual({
type: "reasoning-end",
id: "reasoning-0",
providerMetadata: { bedrock: { redactedData } },
providerMetadata: { "bedrock-converse": { redactedData } },
})
const prepared = yield* compileRequest(
LLM.request({
@@ -1181,8 +1186,8 @@ describe("Bedrock Converse route", () => {
)
expect(response.message.content).toEqual([
{ type: "reasoning", text: "", providerMetadata: { bedrock: { redactedData: "AQI=" } } },
{ type: "reasoning", text: "", providerMetadata: { bedrock: { redactedData: "AwQ=" } } },
{ type: "reasoning", text: "", providerMetadata: { "bedrock-converse": { redactedData: "AQI=" } } },
{ type: "reasoning", text: "", providerMetadata: { "bedrock-converse": { redactedData: "AwQ=" } } },
])
}),
)
@@ -1203,7 +1208,7 @@ describe("Bedrock Converse route", () => {
)
expect(response.message.content).toEqual([
{ type: "reasoning", text: "", providerMetadata: { bedrock: { redactedData: "AQID" } } },
{ type: "reasoning", text: "", providerMetadata: { "bedrock-converse": { redactedData: "AQID" } } },
])
}),
)
@@ -41,8 +41,8 @@ describe("Amazon Bedrock Mantle provider", () => {
protocol: "openai-responses",
body: { model: "openai.gpt-oss-120b", store: false },
})
expect(provider.model("openai.gpt-oss-120b").route.providerMetadataKey).toBe("mantle")
expect(provider.chat("openai.gpt-oss-120b").route.providerMetadataKey).toBe("mantle")
expect(provider.model("openai.gpt-oss-120b").route.id).toBe("bedrock-mantle-responses")
expect(provider.chat("openai.gpt-oss-120b").route.id).toBe("bedrock-mantle-chat")
}),
)
@@ -160,7 +160,7 @@ describe("Amazon Bedrock Mantle provider", () => {
)
expect(response.message.content.find((part) => part.type === "reasoning")?.providerMetadata).toEqual({
mantle: { itemId: "msg_95d4d0af4350432a", reasoningEncryptedContent: "mantle-state" },
"bedrock-mantle-responses": { itemId: "msg_95d4d0af4350432a", reasoningEncryptedContent: "mantle-state" },
})
expect(prepared.body.input).toEqual([
{
@@ -126,7 +126,7 @@ for (const model of [
name: item.type,
result: { type: "json", value: item },
providerExecuted: true,
providerMetadata: { [model.route.providerMetadataKey ?? model.provider]: { itemId: item.id } },
providerMetadata: { [model.route.id]: { itemId: item.id } },
}),
],
})
@@ -196,11 +196,10 @@ const retainedItems = [
for (const model of [
OpenAI.configure({ apiKey: "test" }).responses("gpt-5.3-codex"),
...[undefined, "custom"].map((providerMetadataKey) =>
...["default-metadata", "custom"].map((id) =>
Route.make({
id: providerMetadataKey ?? "default-metadata",
id,
provider: "openai",
providerMetadataKey,
protocol: OpenAIResponses.protocol,
compact: OpenAIResponses.route.compact,
endpoint: OpenAIResponses.route.endpoint,
+13 -13
View File
@@ -493,7 +493,7 @@ describe("Gemini route", () => {
id: "call_image",
name: "read",
input: { path: "pixel.png" },
providerMetadata: { google: { thoughtSignature: "sig_1" } },
providerMetadata: { gemini: { thoughtSignature: "sig_1" } },
}),
]),
Message.tool({
@@ -872,7 +872,7 @@ describe("Gemini route", () => {
reasoningTokens: 1,
totalTokens: 7,
providerMetadata: {
google: {
gemini: {
promptTokenCount: 5,
candidatesTokenCount: 2,
totalTokenCount: 7,
@@ -973,22 +973,22 @@ describe("Gemini route", () => {
{
type: "reasoning",
text: "A",
providerMetadata: { google: { thoughtSignature: "reasoning_sig_a" } },
providerMetadata: { gemini: { thoughtSignature: "reasoning_sig_a" } },
},
{
type: "text",
text: "X",
providerMetadata: { google: { thoughtSignature: "text_sig_x" } },
providerMetadata: { gemini: { thoughtSignature: "text_sig_x" } },
},
{
type: "reasoning",
text: "B",
providerMetadata: { google: { thoughtSignature: "reasoning_sig_b" } },
providerMetadata: { gemini: { thoughtSignature: "reasoning_sig_b" } },
},
{
type: "text",
text: "Y",
providerMetadata: { google: { thoughtSignature: "text_sig_y" } },
providerMetadata: { gemini: { thoughtSignature: "text_sig_y" } },
},
])
}),
@@ -1079,11 +1079,11 @@ describe("Gemini route", () => {
expect(reasoningEnd).toEqual({
type: "reasoning-end",
id: "reasoning-0",
providerMetadata: { google: { thoughtSignature: "thought_sig" } },
providerMetadata: { gemini: { thoughtSignature: "thought_sig" } },
})
expect(toolCall).toMatchObject({
id: "provider_call",
providerMetadata: { google: { thoughtSignature: "tool_sig" } },
providerMetadata: { gemini: { thoughtSignature: "tool_sig" } },
})
expect(response.events.findIndex((event) => event.type === "reasoning-end")).toBeLessThan(
response.events.findIndex((event) => event.type === "tool-call"),
@@ -1153,7 +1153,7 @@ describe("Gemini route", () => {
expect(delta).toMatchObject({
id: "text-0",
text: "All done.",
providerMetadata: { google: { thoughtSignature: "text_sig" } },
providerMetadata: { gemini: { thoughtSignature: "text_sig" } },
})
const prepared = yield* compileRequest(
@@ -1190,7 +1190,7 @@ describe("Gemini route", () => {
expect(delta).toMatchObject({ id: "text-0", text: "Working.", providerMetadata: undefined })
expect(end).toMatchObject({
id: "text-0",
providerMetadata: { google: { thoughtSignature: "tail_sig" } },
providerMetadata: { gemini: { thoughtSignature: "tail_sig" } },
})
}),
)
@@ -1244,7 +1244,7 @@ describe("Gemini route", () => {
id: "tool_0",
name: "lookup",
input: { query: "weather" },
providerMetadata: { google: { thoughtSignature: "parallel_signature" } },
providerMetadata: { gemini: { thoughtSignature: "parallel_signature" } },
}),
ToolCallPart.make({ id: "tool_1", name: "lookup", input: { query: "news" } }),
ToolCallPart.make({ id: "tool_2", name: "lookup", input: { query: "sports" } }),
@@ -1333,7 +1333,7 @@ describe("Gemini route", () => {
cacheReadInputTokens: undefined,
reasoningTokens: undefined,
totalTokens: 6,
providerMetadata: { google: { promptTokenCount: 5, candidatesTokenCount: 1 } },
providerMetadata: { gemini: { promptTokenCount: 5, candidatesTokenCount: 1 } },
})
expect(response.toolCalls[0].id).toMatch(/^tool_[0-9a-zA-Z]+$/)
@@ -1629,7 +1629,7 @@ describe("Gemini route", () => {
type: "finish",
reason: { normalized: "content-filter", raw: "FUTURE_SAFETY_REASON" },
providerMetadata: {
google: {
gemini: {
promptFeedback: {
blockReason: "FUTURE_SAFETY_REASON",
blockReasonMessage: "Prompt blocked",
@@ -11,6 +11,49 @@ import { deltaChunk, finishChunk } from "../lib/openai-chunks.js"
import { sseEvents } from "../lib/sse.js"
describe("Google Vertex providers", () => {
it.effect("round-trips Claude signatures under the Vertex Messages route ID", () =>
Effect.gen(function* () {
const model = GoogleVertexMessages.configure({ project: "project", accessToken: "token" }).model(
"claude-sonnet-4-5",
)
const response = yield* LLMClient.generate(LLM.request({ model, prompt: "Think." })).pipe(
Effect.provide(
fixedResponse(
sseEvents(
{ type: "message_start", message: { usage: { input_tokens: 5 } } },
{ type: "content_block_start", index: 0, content_block: { type: "thinking", thinking: "Thinking." } },
{
type: "content_block_delta",
index: 0,
delta: { type: "signature_delta", signature: "vertex-signature" },
},
{ type: "content_block_stop", index: 0 },
{ type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 2 } },
{ type: "message_stop" },
),
),
),
)
expect(response.message.content).toEqual([
{
type: "reasoning",
text: "Thinking.",
providerMetadata: { "google-vertex-messages": { signature: "vertex-signature" } },
},
])
expect(response.usage?.providerMetadata).toEqual({
"google-vertex-messages": { input_tokens: 5, output_tokens: 2 },
})
const replay = yield* compileRequest(LLM.request({ model, messages: [response.message] }))
expect(replay.body.messages).toEqual([
{
role: "assistant",
content: [{ type: "thinking", thinking: "Thinking.", signature: "vertex-signature" }],
},
])
}),
)
it.effect("sends Gemini requests to the global Vertex endpoint", () =>
Effect.gen(function* () {
const response = yield* LLMClient.generate(
@@ -89,7 +132,7 @@ describe("Google Vertex providers", () => {
id: "call_1",
name: "lookup",
input: { query: "weather" },
providerMetadata: { vertex: { functionCallId: "provider_call_1" } },
providerMetadata: { "google-vertex-gemini": { functionCallId: "provider_call_1" } },
}),
]),
Message.tool({
@@ -97,7 +140,7 @@ describe("Google Vertex providers", () => {
name: "lookup",
result: "sunny",
resultType: "text",
providerMetadata: { vertex: { functionCallId: "provider_call_1" } },
providerMetadata: { "google-vertex-gemini": { functionCallId: "provider_call_1" } },
}),
],
}),
@@ -158,17 +201,17 @@ describe("Google Vertex providers", () => {
const text = response.events.find((event) => event.type === "text-delta")
const toolCall = response.toolCalls[0]
expect(reasoning?.providerMetadata).toEqual({ vertex: { thoughtSignature: "reasoning_sig" } })
expect(text?.providerMetadata).toEqual({ vertex: { thoughtSignature: "text_sig" } })
expect(reasoning?.providerMetadata).toEqual({ "google-vertex-gemini": { thoughtSignature: "reasoning_sig" } })
expect(text?.providerMetadata).toEqual({ "google-vertex-gemini": { thoughtSignature: "text_sig" } })
expect(toolCall).toMatchObject({
id: "provider_call_1",
providerMetadata: { vertex: { thoughtSignature: "tool_sig" } },
providerMetadata: { "google-vertex-gemini": { thoughtSignature: "tool_sig" } },
})
expect(response.usage?.providerMetadata).toEqual({
vertex: { promptTokenCount: 5, candidatesTokenCount: 2, thoughtsTokenCount: 1 },
"google-vertex-gemini": { promptTokenCount: 5, candidatesTokenCount: 2, thoughtsTokenCount: 1 },
})
expect(response.events.at(-1)?.providerMetadata).toEqual({
vertex: { promptFeedback: { blockReasonMessage: "Reviewed" } },
"google-vertex-gemini": { promptFeedback: { blockReasonMessage: "Reviewed" } },
})
const prepared = yield* compileRequest(
@@ -18,8 +18,7 @@ for (const model of [
type: "media",
mediaType: "image/png",
data: "https://example.com/image.png",
providerMetadata:
detail === undefined ? undefined : { [model.route.providerMetadataKey ?? model.provider]: { detail } },
providerMetadata: detail === undefined ? undefined : { [model.route.id]: { detail } },
})),
)
const codec = Schema.fromJsonString(Message)
@@ -50,7 +49,7 @@ it.effect("rejects malformed image detail instead of silently discarding it", ()
type: "media",
mediaType: "image/png",
data: "https://example.com/image.png",
providerMetadata: { openai: { detail: 42 } },
providerMetadata: { "openai-responses": { detail: 42 } },
}),
],
}),
@@ -26,7 +26,6 @@ describe("Mistral Chat", () => {
expect(Mistral.route).toMatchObject({
id: "mistral-chat",
provider: "mistral",
providerMetadataKey: "mistral",
protocol: "mistral-chat",
})
expect(Mistral.route.endpoint).toMatchObject({
@@ -316,7 +315,7 @@ describe("Mistral Chat", () => {
type: "reasoning",
text: "Consider",
providerMetadata: {
mistral: {
"mistral-chat": {
thinking: {
type: "thinking",
thinking: [{ type: "text", text: "Consider" }],
@@ -360,7 +359,7 @@ describe("Mistral Chat", () => {
type: "reasoning",
text: "",
providerMetadata: {
mistral: { thinking: { type: "thinking", thinking: [], marker: "opaque" } },
"mistral-chat": { thinking: { type: "thinking", thinking: [], marker: "opaque" } },
},
},
])
@@ -34,45 +34,48 @@ import { dynamicResponse } from "../lib/http.js"
import { sseEvents } from "../lib/sse.js"
describe("native OpenAI-compatible providers", () => {
test("assigns provider-owned metadata namespaces across native routes", () => {
test("assigns API-specific route identities across native providers", () => {
const vertex = { project: "project", accessToken: "token" }
const providers = [
[OpenAI.configure({ apiKey: "test" }).chat("model"), "openai"],
[OpenAI.configure({ apiKey: "test" }).responses("model"), "openai"],
[Azure.configure({ resourceName: "resource", apiKey: "test" }).chat("model"), "azure"],
[Azure.configure({ resourceName: "resource", apiKey: "test" }).responses("model"), "azure"],
[AmazonBedrock.configure({ apiKey: "test" }).model("model"), "bedrock"],
[AmazonBedrockMantle.configure({ apiKey: "test" }).chat("model"), "mantle"],
[AmazonBedrockMantle.configure({ apiKey: "test" }).responses("model"), "mantle"],
[Google.configure({ apiKey: "test" }).model("model"), "google"],
[GoogleVertex.configure(vertex).model("model"), "vertex"],
[GoogleVertexChat.configure(vertex).model("model"), "vertex"],
[GoogleVertexResponses.configure(vertex).model("model"), "vertex"],
[GoogleVertexMessages.configure(vertex).model("model"), "anthropic"],
[Anthropic.configure({ apiKey: "test" }).model("model"), "anthropic"],
[OpenAI.configure({ apiKey: "test" }).chat("model"), "openai-chat"],
[OpenAI.configure({ apiKey: "test" }).responses("model"), "openai-responses"],
[Azure.configure({ resourceName: "resource", apiKey: "test" }).chat("model"), "azure-openai-chat"],
[Azure.configure({ resourceName: "resource", apiKey: "test" }).responses("model"), "azure-openai-responses"],
[AmazonBedrock.configure({ apiKey: "test" }).model("model"), "bedrock-converse"],
[AmazonBedrockMantle.configure({ apiKey: "test" }).chat("model"), "bedrock-mantle-chat"],
[AmazonBedrockMantle.configure({ apiKey: "test" }).responses("model"), "bedrock-mantle-responses"],
[Google.configure({ apiKey: "test" }).model("model"), "gemini"],
[GoogleVertex.configure(vertex).model("model"), "google-vertex-gemini"],
[GoogleVertexChat.configure(vertex).model("model"), "google-vertex-chat"],
[GoogleVertexResponses.configure(vertex).model("model"), "google-vertex-responses"],
[GoogleVertexMessages.configure(vertex).model("model"), "google-vertex-messages"],
[Anthropic.configure({ apiKey: "test" }).model("model"), "anthropic-messages"],
[
AnthropicCompatible.configure({ baseURL: "https://example.test/v1", provider: "minimax" }).model("model"),
"minimax",
"anthropic-messages",
],
[
OpenAICompatible.configure({ baseURL: "https://example.test/v1", provider: "custom" }).model("model"),
"openai-compatible-chat",
],
[OpenAICompatible.configure({ baseURL: "https://example.test/v1", provider: "custom" }).model("model"), "custom"],
[
OpenAICompatibleResponses.configure({ baseURL: "https://example.test/v1", provider: "custom" }).model("model"),
"custom",
"openai-compatible-responses",
],
[Cerebras.configure({ apiKey: "test" }).model("model"), "cerebras"],
[Baseten.configure({ apiKey: "test" }).model("model"), "baseten"],
[DeepSeek.configure({ apiKey: "test" }).model("model"), "deepseek"],
[Fireworks.configure({ apiKey: "test" }).model("model"), "fireworks"],
[DeepInfra.configure({ apiKey: "test" }).model("model"), "deepinfra"],
[TogetherAI.configure({ apiKey: "test" }).model("model"), "togetherai"],
[Cerebras.configure({ apiKey: "test" }).model("model"), "cerebras-chat"],
[Baseten.configure({ apiKey: "test" }).model("model"), "baseten-chat"],
[DeepSeek.configure({ apiKey: "test" }).model("model"), "deepseek-chat"],
[Fireworks.configure({ apiKey: "test" }).model("model"), "fireworks-chat"],
[DeepInfra.configure({ apiKey: "test" }).model("model"), "deepinfra-chat"],
[TogetherAI.configure({ apiKey: "test" }).model("model"), "togetherai-chat"],
[CloudflareAIGateway.configure({ accountId: "account" }).model("model"), "cloudflare-ai-gateway"],
[CloudflareWorkersAI.configure({ accountId: "account" }).model("model"), "cloudflare-workers-ai"],
[OpenRouter.configure({ apiKey: "test" }).model("model"), "openrouter"],
[XAI.configure({ apiKey: "test" }).chat("model"), "xai"],
[XAI.configure({ apiKey: "test" }).responses("model"), "xai"],
[XAI.configure({ apiKey: "test" }).chat("model"), "xai-chat"],
[XAI.configure({ apiKey: "test" }).responses("model"), "xai-responses"],
] as const
for (const [model, key] of providers) expect(model.route.providerMetadataKey).toBe(key)
for (const [model, id] of providers) expect(model.route.id).toBe(id)
})
test("preserves native Together AI and Cerebras provider and route identities", () => {
@@ -37,7 +37,7 @@ describe("Open Responses completed item text", () => {
type: "text-end",
id: "msg_1",
text,
providerMetadata: { "openai-compatible": { itemId: "msg_1", phase: "final_answer" } },
providerMetadata: { "openai-compatible-responses": { itemId: "msg_1", phase: "final_answer" } },
},
])
}),
@@ -138,7 +138,7 @@ describe("Open Responses completed item reasoning", () => {
expect(response.reasoning).toBe(fixture.text)
expect(response.events.filter(LLMEvent.is.reasoningEnd)).toHaveLength(1)
expect(response.message.content.find((part) => part.type === "reasoning")?.providerMetadata).toEqual({
"openai-compatible": { itemId: "rs_1", reasoningEncryptedContent: "encrypted" },
"openai-compatible-responses": { itemId: "rs_1", reasoningEncryptedContent: "encrypted" },
})
}),
)
@@ -97,27 +97,37 @@ describe("Open Responses basic-item lifecycles", () => {
{
type: "reasoning-start",
id: "rs_1:0",
providerMetadata: { "openai-compatible": { itemId: "rs_1", reasoningEncryptedContent: null } },
providerMetadata: { "openai-compatible-responses": { itemId: "rs_1", reasoningEncryptedContent: null } },
},
{ type: "reasoning-delta", id: "rs_1:0", text: "First" },
{ type: "reasoning-end", id: "rs_1:0", providerMetadata: { "openai-compatible": { itemId: "rs_1" } } },
{
type: "reasoning-end",
id: "rs_1:0",
providerMetadata: { "openai-compatible-responses": { itemId: "rs_1" } },
},
{
type: "reasoning-start",
id: "rs_1:1",
providerMetadata: { "openai-compatible": { itemId: "rs_1", reasoningEncryptedContent: null } },
providerMetadata: { "openai-compatible-responses": { itemId: "rs_1", reasoningEncryptedContent: null } },
},
{ type: "reasoning-delta", id: "rs_1:1", text: "Second" },
{ type: "reasoning-end", id: "rs_1:1", providerMetadata: { "openai-compatible": { itemId: "rs_1" } } },
{
type: "reasoning-end",
id: "rs_1:1",
providerMetadata: { "openai-compatible-responses": { itemId: "rs_1" } },
},
{
type: "reasoning-start",
id: "rs_1:2",
providerMetadata: { "openai-compatible": { itemId: "rs_1", reasoningEncryptedContent: null } },
providerMetadata: { "openai-compatible-responses": { itemId: "rs_1", reasoningEncryptedContent: null } },
},
{ type: "reasoning-delta", id: "rs_1:2", text: "Third" },
{
type: "reasoning-end",
id: "rs_1:2",
providerMetadata: { "openai-compatible": { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
providerMetadata: {
"openai-compatible-responses": { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" },
},
},
])
}),
@@ -141,13 +151,17 @@ describe("Open Responses basic-item lifecycles", () => {
{
type: "reasoning-start",
id: "rs_1",
providerMetadata: { "openai-compatible": { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
providerMetadata: {
"openai-compatible-responses": { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" },
},
},
{
type: "reasoning-end",
id: "rs_1",
text: "Not streamed",
providerMetadata: { "openai-compatible": { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
providerMetadata: {
"openai-compatible-responses": { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" },
},
},
])
}),
@@ -176,32 +190,36 @@ describe("Open Responses basic-item lifecycles", () => {
{
type: "text-start",
id: "msg_1",
providerMetadata: { "openai-compatible": { itemId: "msg_1", phase: "commentary" } },
providerMetadata: { "openai-compatible-responses": { itemId: "msg_1", phase: "commentary" } },
},
{ type: "text-delta", id: "msg_1", text: "Checking" },
{
type: "text-end",
id: "msg_1",
providerMetadata: { "openai-compatible": { itemId: "msg_1", phase: "commentary" } },
providerMetadata: { "openai-compatible-responses": { itemId: "msg_1", phase: "commentary" } },
},
{
type: "text-start",
id: "msg_2",
providerMetadata: { "openai-compatible": { itemId: "msg_2", phase: null } },
providerMetadata: { "openai-compatible-responses": { itemId: "msg_2", phase: null } },
},
{ type: "text-delta", id: "msg_2", text: "Cannot help." },
{
type: "text-end",
id: "msg_2",
providerMetadata: { "openai-compatible": { itemId: "msg_2", phase: "final_answer" } },
providerMetadata: { "openai-compatible-responses": { itemId: "msg_2", phase: "final_answer" } },
},
{
type: "text-start",
id: "msg_3",
providerMetadata: { "openai-compatible": { itemId: "msg_3", phase: null } },
providerMetadata: { "openai-compatible-responses": { itemId: "msg_3", phase: null } },
},
{ type: "text-delta", id: "msg_3", text: "Done-only refusal." },
{ type: "text-end", id: "msg_3", providerMetadata: { "openai-compatible": { itemId: "msg_3", phase: null } } },
{
type: "text-end",
id: "msg_3",
providerMetadata: { "openai-compatible-responses": { itemId: "msg_3", phase: null } },
},
])
}),
)
@@ -232,24 +250,24 @@ describe("Open Responses basic-item lifecycles", () => {
{
type: "text-start",
id: "msg_text",
providerMetadata: { "openai-compatible": { itemId: "msg_text" } },
providerMetadata: { "openai-compatible-responses": { itemId: "msg_text" } },
},
{
type: "text-end",
id: "msg_text",
text: "Done-only text.",
providerMetadata: { "openai-compatible": { itemId: "msg_text" } },
providerMetadata: { "openai-compatible-responses": { itemId: "msg_text" } },
},
{
type: "text-start",
id: "msg_refusal",
providerMetadata: { "openai-compatible": { itemId: "msg_refusal" } },
providerMetadata: { "openai-compatible-responses": { itemId: "msg_refusal" } },
},
{
type: "text-end",
id: "msg_refusal",
text: "Done-only refusal.",
providerMetadata: { "openai-compatible": { itemId: "msg_refusal" } },
providerMetadata: { "openai-compatible-responses": { itemId: "msg_refusal" } },
},
])
}),
@@ -305,7 +323,7 @@ describe("Open Responses basic-item lifecycles", () => {
response: { id: "resp_1", output: [{ type: "reasoning", id: "msg_879a68b589198b4c" }, terminal] },
},
)
const providerMetadata = { "openai-compatible": { itemId: "fc_828bee50dee1d029" } }
const providerMetadata = { "openai-compatible-responses": { itemId: "fc_828bee50dee1d029" } }
expect(events.filter((event) => event.type.startsWith("tool-"))).toEqual([
{ type: "tool-input-start", id: "call_bc1eb4b42e70ee53", name: "get_weather", providerMetadata },
{
@@ -338,7 +356,7 @@ describe("Open Responses basic-item lifecycles", () => {
)
const call = events.find(LLMEvent.is.toolCall)
expect(call).toMatchObject({ id: "call_1", name: "lookup", input: { query: "weather" } })
expect(call?.providerMetadata?.["openai-compatible"]).toMatchObject({
expect(call?.providerMetadata?.["openai-compatible-responses"]).toMatchObject({
itemId: expect.stringMatching(/^fc_[0-9a-f]{32}$/),
})
}),
@@ -354,7 +372,7 @@ describe("Open Responses basic-item lifecycles", () => {
arguments: '{"query":"weather"}',
}
const events = yield* collect({ type: "response.output_item.done", item }, completed)
const providerMetadata = { "openai-compatible": { itemId: "fc_1" } }
const providerMetadata = { "openai-compatible-responses": { itemId: "fc_1" } }
expect(events.filter((event) => event.type.startsWith("tool-"))).toEqual([
{ type: "tool-input-start", id: "call_1", name: "lookup", namespace: undefined, providerMetadata },
{ type: "tool-input-end", id: "call_1", name: "lookup", namespace: undefined, providerMetadata },
@@ -371,7 +389,7 @@ describe("Open Responses basic-item lifecycles", () => {
{
type: "finish",
reason: { normalized: "tool-calls", raw: undefined },
providerMetadata: { "openai-compatible": { responseId: "resp_1", serviceTier: undefined } },
providerMetadata: { "openai-compatible-responses": { responseId: "resp_1", serviceTier: undefined } },
},
])
}),
@@ -405,7 +423,7 @@ describe("Open Responses basic-item lifecycles", () => {
type: "tool-input-end",
id: "call_1",
name: "lookup",
providerMetadata: { "openai-compatible": { itemId: "fc_1" } },
providerMetadata: { "openai-compatible-responses": { itemId: "fc_1" } },
},
{
type: "tool-call",
@@ -413,7 +431,7 @@ describe("Open Responses basic-item lifecycles", () => {
name: "lookup",
input: { query: "final" },
providerExecuted: undefined,
providerMetadata: { "openai-compatible": { itemId: "fc_1" } },
providerMetadata: { "openai-compatible-responses": { itemId: "fc_1" } },
},
{ type: "reasoning-end", id: "rs_1:0" },
])
@@ -438,7 +456,7 @@ describe("Open Responses basic-item lifecycles", () => {
{
type: "reasoning-end",
id: ":0",
providerMetadata: { "openai-compatible": { itemId: "", reasoningEncryptedContent: "state" } },
providerMetadata: { "openai-compatible-responses": { itemId: "", reasoningEncryptedContent: "state" } },
},
])
}),
@@ -474,7 +492,7 @@ describe("Open Responses basic-item lifecycles", () => {
completed,
)
// Generic terminal closure does not repeat the message's phase metadata.
const providerMetadata = { "openai-compatible": { itemId: "fc_1" } }
const providerMetadata = { "openai-compatible-responses": { itemId: "fc_1" } }
expect(events.slice(4, -2)).toEqual([
{ type: "tool-input-end", id: "call_1", name: "lookup", providerMetadata },
{ type: "tool-call", id: "call_1", name: "lookup", input: {}, providerMetadata },
@@ -518,7 +536,7 @@ describe("Open Responses basic-item lifecycles", () => {
{
type: "finish",
reason: { normalized: "length", raw: "max_output_tokens" },
providerMetadata: { "openai-compatible": { responseId: "resp_1", serviceTier: undefined } },
providerMetadata: { "openai-compatible-responses": { responseId: "resp_1", serviceTier: undefined } },
},
])
}),
@@ -69,7 +69,7 @@ for (const item of cases) {
expect(response.reasoning.length).toBeGreaterThan(0)
expect(response.events.some(LLMEvent.is.reasoningDelta)).toBe(true)
const metadata = response.message.content.find((part) => part.type === "reasoning")?.providerMetadata?.[
item.model.route.providerMetadataKey ?? String(item.model.provider)
item.model.route.id
]
expect(metadata?.reasoningField).toBe(item.structured ? "reasoning" : "reasoning_content")
expect(Array.isArray(metadata?.reasoningDetails)).toBe(item.structured)
@@ -128,11 +128,7 @@ for (const item of cases) {
).toMatch(/^Paris is sunny\.?$/)
const details = events
.filter(LLMEvent.is.reasoningEnd)
.map(
(event) =>
event.providerMetadata?.[item.model.route.providerMetadataKey ?? String(item.model.provider)]
?.reasoningDetails,
)
.map((event) => event.providerMetadata?.[item.model.route.id]?.reasoningDetails)
.find(Array.isArray)
expect(Array.isArray(details)).toBe(item.structured)
if (!item.structured || !Array.isArray(details)) return
+28 -29
View File
@@ -154,7 +154,7 @@ describe("OpenAI Chat route", () => {
{
type: "reasoning",
text: "thinking",
providerMetadata: { openai: { reasoningField: "reasoning" } },
providerMetadata: { "openai-chat": { reasoningField: "reasoning" } },
},
{ type: "text", text: "Hello" },
]),
@@ -180,7 +180,7 @@ describe("OpenAI Chat route", () => {
{
type: "reasoning",
text: "thinking",
providerMetadata: { openai: { reasoningField: "reasoning_text" } },
providerMetadata: { "openai-chat": { reasoningField: "reasoning_text" } },
},
{ type: "text", text: "Hello" },
]),
@@ -775,7 +775,7 @@ describe("OpenAI Chat route", () => {
reasoningTokens: 0,
totalTokens: 7,
providerMetadata: {
openai: {
"openai-chat": {
prompt_tokens: 5,
completion_tokens: 2,
total_tokens: 7,
@@ -868,7 +868,7 @@ describe("OpenAI Chat route", () => {
)
expect(response.message.content).toEqual([
{ type: "reasoning", text: "", providerMetadata: { openai: { reasoningDetails: [] } } },
{ type: "reasoning", text: "", providerMetadata: { "openai-chat": { reasoningDetails: [] } } },
{
type: "text",
text: "I can't help with that.",
@@ -917,7 +917,7 @@ describe("OpenAI Chat route", () => {
expect(response.reasoning).toBe("thinking")
expect(response.text).toBe("Hello")
expect(response.message.content.find((part) => part.type === "reasoning")?.providerMetadata).toEqual({
openai: { reasoningField: field },
"openai-chat": { reasoningField: field },
})
const replay = yield* compileRequest(LLM.request({ model, messages: [response.message] }))
@@ -926,10 +926,10 @@ describe("OpenAI Chat route", () => {
}),
)
it.effect("uses the configured provider metadata namespace for reasoning and usage", () =>
it.effect("uses the route ID for reasoning and usage metadata", () =>
Effect.gen(function* () {
const selected = LanguageModel.update(model, {
route: { ...model.route, providerMetadataKey: "vendor" },
route: model.route.with({ id: "vendor" }),
})
const details = [{ type: "reasoning.text", text: "thinking", signature: "signed" }]
const response = yield* LLMClient.generate(LLMRequest.update(request, { model: selected })).pipe(
@@ -959,12 +959,9 @@ describe("OpenAI Chat route", () => {
}),
)
it.effect("falls back to the selected provider for the metadata namespace", () =>
it.effect("preserves the route metadata namespace when the selected provider changes", () =>
Effect.gen(function* () {
const compatible = model.route.with({ provider: "deepseek" }).model({ id: "deepseek-chat" })
const selected = LanguageModel.update(compatible, {
route: { ...compatible.route, providerMetadataKey: undefined },
})
const selected = model.route.with({ provider: "deepseek" }).model({ id: "deepseek-chat" })
const response = yield* LLMClient.generate(LLMRequest.update(request, { model: selected })).pipe(
Effect.provide(
fixedResponse(
@@ -979,10 +976,10 @@ describe("OpenAI Chat route", () => {
)
expect(response.message.content.find((part) => part.type === "reasoning")?.providerMetadata).toEqual({
deepseek: { reasoningField: "reasoning_content" },
"openai-chat": { reasoningField: "reasoning_content" },
})
expect(response.usage?.providerMetadata).toEqual({
deepseek: { prompt_tokens: 5, completion_tokens: 2, total_tokens: 7 },
"openai-chat": { prompt_tokens: 5, completion_tokens: 2, total_tokens: 7 },
})
const replay = yield* compileRequest(LLM.request({ model: selected, messages: [response.message] }))
@@ -1007,7 +1004,7 @@ describe("OpenAI Chat route", () => {
expect(response.reasoning).toBe("thinking")
expect(response.message.content.find((part) => part.type === "reasoning")?.providerMetadata).toEqual({
openai: { reasoningField: "vendor_reasoning" },
"openai-chat": { reasoningField: "vendor_reasoning" },
})
const replay = yield* compileRequest(LLM.request({ model: custom, messages: [response.message] }))
@@ -1050,7 +1047,7 @@ describe("OpenAI Chat route", () => {
expect(response.reasoning).toBe("thinking")
expect(response.message.content.find((part) => part.type === "reasoning")?.providerMetadata).toEqual({
openai: { reasoningField: "reasoning", reasoningDetails: details },
"openai-chat": { reasoningField: "reasoning", reasoningDetails: details },
})
const replay = yield* compileRequest(LLM.request({ model, messages: [response.message] }))
@@ -1093,7 +1090,7 @@ describe("OpenAI Chat route", () => {
expect(response.reasoning).toBe("thinking")
expect(response.message.content.find((part) => part.type === "reasoning")?.providerMetadata).toEqual({
openai: { reasoningDetails: details },
"openai-chat": { reasoningDetails: details },
})
const replay = yield* compileRequest(LLM.request({ model, messages: [response.message] }))
@@ -1118,7 +1115,7 @@ describe("OpenAI Chat route", () => {
expect(response.reasoning).toBe("thinking")
expect(response.message.content.find((part) => part.type === "reasoning")?.providerMetadata).toEqual({
openai: { reasoningField: "reasoning", reasoningDetails: details },
"openai-chat": { reasoningField: "reasoning", reasoningDetails: details },
})
const replay = yield* compileRequest(LLM.request({ model, messages: [response.message] }))
@@ -1145,7 +1142,7 @@ describe("OpenAI Chat route", () => {
expect(response.reasoning).toBe("thinking")
expect(response.message.content.find((part) => part.type === "reasoning")?.providerMetadata).toEqual({
openai: { reasoningField: "reasoning", reasoningDetails: details },
"openai-chat": { reasoningField: "reasoning", reasoningDetails: details },
})
}),
)
@@ -1171,7 +1168,7 @@ describe("OpenAI Chat route", () => {
expect(response.events.filter(LLMEvent.is.reasoningEnd)).toHaveLength(1)
expect(response.message.content.filter((part) => part.type === "reasoning")).toHaveLength(1)
expect(response.message.content.find((part) => part.type === "reasoning")?.providerMetadata).toEqual({
openai: { reasoningField: "reasoning", reasoningDetails: details },
"openai-chat": { reasoningField: "reasoning", reasoningDetails: details },
})
}),
)
@@ -1197,7 +1194,7 @@ describe("OpenAI Chat route", () => {
expect(response.events.filter(LLMEvent.is.reasoningEnd)).toHaveLength(1)
expect(response.message.content.filter((part) => part.type === "reasoning")).toHaveLength(1)
expect(response.message.content.find((part) => part.type === "reasoning")?.providerMetadata).toEqual({
openai: { reasoningField: "reasoning", reasoningDetails: details },
"openai-chat": { reasoningField: "reasoning", reasoningDetails: details },
})
}),
)
@@ -1218,7 +1215,7 @@ describe("OpenAI Chat route", () => {
expect(response.reasoning).toBe("")
expect(response.message.content.find((part) => part.type === "reasoning")?.providerMetadata).toEqual({
openai: { reasoningDetails: [] },
"openai-chat": { reasoningDetails: [] },
})
const replay = yield* compileRequest(LLM.request({ model, messages: [response.message] }))
@@ -1268,12 +1265,14 @@ describe("OpenAI Chat route", () => {
expect(response.reasoning).toBe("thinking")
expect(response.message.content.filter((part) => part.type === "reasoning")).toHaveLength(1)
expect(response.message.content.find((part) => part.type === "reasoning")?.providerMetadata).toEqual({
openai: { reasoningField: "reasoning", reasoningDetails: merged },
"openai-chat": { reasoningField: "reasoning", reasoningDetails: merged },
})
expect(response.events.filter(LLMEvent.is.reasoningStart)).toHaveLength(1)
expect(response.events.filter(LLMEvent.is.reasoningDelta)).toHaveLength(1)
expect(response.events.filter(LLMEvent.is.reasoningEnd)).toHaveLength(1)
expect(publishedEndMetadata).toEqual([{ openai: { reasoningField: "reasoning", reasoningDetails: merged } }])
expect(publishedEndMetadata).toEqual([
{ "openai-chat": { reasoningField: "reasoning", reasoningDetails: merged } },
])
expect(response.events.findIndex(LLMEvent.is.reasoningStart)).toBeLessThan(
response.events.findIndex(LLMEvent.is.textStart),
)
@@ -1304,7 +1303,7 @@ describe("OpenAI Chat route", () => {
)
expect(response.message.content).toEqual([
{ type: "reasoning", text: "", providerMetadata: { openai: { reasoningDetails: details } } },
{ type: "reasoning", text: "", providerMetadata: { "openai-chat": { reasoningDetails: details } } },
])
expect(response.events.filter(LLMEvent.is.reasoningStart)).toHaveLength(1)
expect(response.events.filter(LLMEvent.is.reasoningEnd)).toHaveLength(1)
@@ -1330,7 +1329,7 @@ describe("OpenAI Chat route", () => {
expect(response.reasoning).toBe("summary")
expect(response.message.content).toEqual([
{ type: "reasoning", text: "summary", providerMetadata: { openai: { reasoningDetails: details } } },
{ type: "reasoning", text: "summary", providerMetadata: { "openai-chat": { reasoningDetails: details } } },
])
}),
)
@@ -1347,12 +1346,12 @@ describe("OpenAI Chat route", () => {
{
type: "reasoning",
text: "first",
providerMetadata: { openai: { reasoningDetails: [first] } },
providerMetadata: { "openai-chat": { reasoningDetails: [first] } },
},
{
type: "reasoning",
text: "second",
providerMetadata: { openai: { reasoningField: "reasoning", reasoningDetails: [second] } },
providerMetadata: { "openai-chat": { reasoningField: "reasoning", reasoningDetails: [second] } },
},
]),
],
@@ -1376,7 +1375,7 @@ describe("OpenAI Chat route", () => {
{
type: "reasoning",
text: "A",
providerMetadata: { openai: { reasoningDetails: [detail] } },
providerMetadata: { "openai-chat": { reasoningDetails: [detail] } },
},
{ type: "reasoning", text: "B" },
]),
@@ -405,7 +405,7 @@ describe("OpenAI-compatible Chat route", () => {
outputTokens: undefined,
totalTokens: undefined,
providerMetadata: {
deepseek: {
"openai-compatible-chat": {
prompt_tokens: null,
completion_tokens: null,
total_tokens: null,
@@ -276,19 +276,31 @@ describe("Open Responses-compatible route", () => {
model,
messages: [
Message.assistant([
{ type: "text", text: "Kept.", providerMetadata: { "openai-compatible": { itemId: "history_1" } } },
{
type: "text",
text: "Kept.",
providerMetadata: { "openai-compatible-responses": { itemId: "history_1" } },
},
{
type: "text",
text: "Long.",
providerMetadata: { "openai-compatible": { itemId: `history_${"a".repeat(64)}` } },
providerMetadata: { "openai-compatible-responses": { itemId: `history_${"a".repeat(64)}` } },
},
{
type: "text",
text: "Opaque.",
providerMetadata: { "openai-compatible": { itemId: "provider_value/with+symbols" } },
providerMetadata: { "openai-compatible-responses": { itemId: "provider_value/with+symbols" } },
},
{
type: "text",
text: "No suffix.",
providerMetadata: { "openai-compatible-responses": { itemId: "msg_" } },
},
{
type: "text",
text: "No prefix.",
providerMetadata: { "openai-compatible-responses": { itemId: "_item" } },
},
{ type: "text", text: "No suffix.", providerMetadata: { "openai-compatible": { itemId: "msg_" } } },
{ type: "text", text: "No prefix.", providerMetadata: { "openai-compatible": { itemId: "_item" } } },
]),
],
}),
@@ -348,7 +360,7 @@ describe("Open Responses-compatible route", () => {
name: item.type,
result: { type: "json", value: item },
providerExecuted: true,
providerMetadata: { example: { itemId: item.id } },
providerMetadata: { "openai-compatible-responses": { itemId: item.id } },
}),
),
}),
@@ -383,7 +395,7 @@ describe("Open Responses-compatible route", () => {
)
expect(response.message.content).toEqual([
{ type: "text", text: "Indexed", providerMetadata: { "openai-compatible": { itemId: "msg_1" } } },
{ type: "text", text: "Indexed", providerMetadata: { "openai-compatible-responses": { itemId: "msg_1" } } },
])
}),
)
@@ -449,7 +461,7 @@ describe("Open Responses-compatible route", () => {
),
)
const metadata = { "openai-compatible": { itemId: routing.id } }
const metadata = { "openai-compatible-responses": { itemId: routing.id } }
if (fixture.item.type === "function_call") {
expect(response.toolCalls).toEqual([
expect.objectContaining({
@@ -467,7 +479,10 @@ describe("Open Responses-compatible route", () => {
type: "reasoning",
text: "Preserved",
providerMetadata: {
"openai-compatible": { itemId: routing.id, reasoningEncryptedContent: "encrypted-state" },
"openai-compatible-responses": {
itemId: routing.id,
reasoningEncryptedContent: "encrypted-state",
},
},
},
])
@@ -519,13 +534,13 @@ describe("Open Responses-compatible route", () => {
{
type: "reasoning",
text: "First.",
providerMetadata: { "openai-compatible": { itemId: routing.id } },
providerMetadata: { "openai-compatible-responses": { itemId: routing.id } },
},
{
type: "reasoning",
text: "Second.",
providerMetadata: {
"openai-compatible": { itemId: routing.id, reasoningEncryptedContent: null },
"openai-compatible-responses": { itemId: routing.id, reasoningEncryptedContent: null },
},
},
])
@@ -534,7 +549,7 @@ describe("Open Responses-compatible route", () => {
type: "reasoning-end",
id: `${routing.id}:0`,
text: undefined,
providerMetadata: { "openai-compatible": { itemId: routing.id } },
providerMetadata: { "openai-compatible-responses": { itemId: routing.id } },
},
{ type: "reasoning-end", id: `${routing.id}:1` },
])
@@ -565,7 +580,7 @@ describe("Open Responses-compatible route", () => {
id: "call_1",
name: "lookup",
input: { query: "complete" },
providerMetadata: { "openai-compatible": { itemId: "" } },
providerMetadata: { "openai-compatible-responses": { itemId: "" } },
}),
])
}),
@@ -592,7 +607,11 @@ describe("Open Responses-compatible route", () => {
)
expect(response.message.content).toEqual([
{ type: "text", text: "Before after", providerMetadata: { "openai-compatible": { itemId: "msg_1" } } },
{
type: "text",
text: "Before after",
providerMetadata: { "openai-compatible-responses": { itemId: "msg_1" } },
},
])
expect(response.events.map((event) => event.type)).toEqual([
"step-start",
@@ -705,7 +724,7 @@ describe("Open Responses-compatible route", () => {
expect(response.events.find(LLMEvent.is.toolCall)).toMatchObject({
input: { query: "complete" },
providerMetadata: { example: { itemId: "item_1" } },
providerMetadata: { "openai-compatible-responses": { itemId: "item_1" } },
})
}),
)
@@ -787,7 +806,7 @@ describe("Open Responses-compatible route", () => {
Message.assistant({
type: "text",
text: "Unclassified.",
providerMetadata: { "openai-compatible": { phase: null } },
providerMetadata: { "openai-compatible-responses": { phase: null } },
}),
],
}),
@@ -846,7 +865,7 @@ describe("Open Responses-compatible route", () => {
{
type: "text",
text: "I can't help with that.",
providerMetadata: { example: { itemId: "msg_refusal" } },
providerMetadata: { "openai-compatible-responses": { itemId: "msg_refusal" } },
},
])
@@ -935,7 +954,7 @@ describe("Open Responses-compatible route", () => {
expect(response.toolCalls).toEqual([])
expect(response.events.find(LLMEvent.is.finish)).toMatchObject({
providerMetadata: { example: { responseId: "resp_1" } },
providerMetadata: { "openai-compatible-responses": { responseId: "resp_1" } },
})
}),
)
@@ -1938,7 +1938,7 @@ describe("OpenAI Responses route", () => {
reasoningTokens: 0,
totalTokens: 7,
providerMetadata: {
openai: {
"openai-responses": {
input_tokens: 5,
output_tokens: 2,
total_tokens: 7,
@@ -1951,7 +1951,7 @@ describe("OpenAI Responses route", () => {
expect(response.text).toBe("Hello!")
expect(response.events).toEqual([
{ type: "step-start", index: 0 },
{ type: "text-start", id: "msg_1", providerMetadata: { openai: { itemId: "msg_1" } } },
{ type: "text-start", id: "msg_1", providerMetadata: { "openai-responses": { itemId: "msg_1" } } },
{ type: "text-delta", id: "msg_1", text: "Hello" },
{ type: "text-delta", id: "msg_1", text: "!" },
{ type: "text-end", id: "msg_1" },
@@ -1959,13 +1959,13 @@ describe("OpenAI Responses route", () => {
type: "step-finish",
index: 0,
reason: { normalized: "stop", raw: undefined },
providerMetadata: { openai: { responseId: "resp_1", serviceTier: "default" } },
providerMetadata: { "openai-responses": { responseId: "resp_1", serviceTier: "default" } },
usage,
},
{
type: "finish",
reason: { normalized: "stop", raw: undefined },
providerMetadata: { openai: { responseId: "resp_1", serviceTier: "default" } },
providerMetadata: { "openai-responses": { responseId: "resp_1", serviceTier: "default" } },
usage,
},
])
@@ -2040,7 +2040,7 @@ describe("OpenAI Responses route", () => {
{
type: "text",
text: "I can't help with that.",
providerMetadata: { openai: { itemId: "msg_refusal", phase: "final_answer" } },
providerMetadata: { "openai-responses": { itemId: "msg_refusal", phase: "final_answer" } },
},
])
@@ -2112,17 +2112,17 @@ describe("OpenAI Responses route", () => {
{
type: "text",
text: "Checking.",
providerMetadata: { openai: { itemId: "msg_commentary", phase: "commentary" } },
providerMetadata: { "openai-responses": { itemId: "msg_commentary", phase: "commentary" } },
},
{
type: "text",
text: "Finished.",
providerMetadata: { openai: { itemId: "msg_final", phase: "final_answer" } },
providerMetadata: { "openai-responses": { itemId: "msg_final", phase: "final_answer" } },
},
{
type: "text",
text: "Unclassified.",
providerMetadata: { openai: { itemId: "msg_null", phase: null } },
providerMetadata: { "openai-responses": { itemId: "msg_null", phase: null } },
},
])
@@ -2170,7 +2170,7 @@ describe("OpenAI Responses route", () => {
expect(response.text).toBe("Indexed")
expect(response.message.content).toEqual([
{ type: "text", text: "Indexed", providerMetadata: { openai: { itemId: "msg_1" } } },
{ type: "text", text: "Indexed", providerMetadata: { "openai-responses": { itemId: "msg_1" } } },
])
}),
)
@@ -2331,7 +2331,7 @@ describe("OpenAI Responses route", () => {
{
type: "reasoning",
text: "Thinking",
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: "state" } },
providerMetadata: { "openai-responses": { itemId: "rs_1", reasoningEncryptedContent: "state" } },
},
])
}),
@@ -2379,7 +2379,7 @@ describe("OpenAI Responses route", () => {
{
type: "reasoning",
text: "Raw",
providerMetadata: { openai: { itemId: "", reasoningEncryptedContent: "state" } },
providerMetadata: { "openai-responses": { itemId: "", reasoningEncryptedContent: "state" } },
},
])
}),
@@ -2588,12 +2588,12 @@ describe("OpenAI Responses route", () => {
)
expect(response.events.filter((event) => event.type.startsWith("text-"))).toEqual([
{ type: "text-start", id: "msg_1", providerMetadata: { openai: { itemId: "msg_1" } } },
{ type: "text-start", id: "msg_1", providerMetadata: { "openai-responses": { itemId: "msg_1" } } },
{ type: "text-delta", id: "msg_1", text: "First", providerMetadata: undefined },
{ type: "text-end", id: "msg_1", providerMetadata: { openai: { itemId: "msg_1" } } },
{ type: "text-start", id: "msg_2", providerMetadata: { openai: { itemId: "msg_2" } } },
{ type: "text-end", id: "msg_1", providerMetadata: { "openai-responses": { itemId: "msg_1" } } },
{ type: "text-start", id: "msg_2", providerMetadata: { "openai-responses": { itemId: "msg_2" } } },
{ type: "text-delta", id: "msg_2", text: "Second", providerMetadata: undefined },
{ type: "text-end", id: "msg_2", providerMetadata: { openai: { itemId: "msg_2" } } },
{ type: "text-end", id: "msg_2", providerMetadata: { "openai-responses": { itemId: "msg_2" } } },
])
}),
)
@@ -2629,9 +2629,9 @@ describe("OpenAI Responses route", () => {
{
type: "reasoning",
text: "thinking",
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: null } },
providerMetadata: { "openai-responses": { itemId: "rs_1", reasoningEncryptedContent: null } },
},
{ type: "text", text: "Hello", providerMetadata: { openai: { itemId: "msg_1" } } },
{ type: "text", text: "Hello", providerMetadata: { "openai-responses": { itemId: "msg_1" } } },
])
}),
)
@@ -2663,7 +2663,7 @@ describe("OpenAI Responses route", () => {
expect.objectContaining({
type: "reasoning-end",
id: "rs_1:0",
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
providerMetadata: { "openai-responses": { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
}),
)
}),
@@ -2715,7 +2715,7 @@ describe("OpenAI Responses route", () => {
expect(response.message.content).toContainEqual({
type: "reasoning",
text: "Checked the diff.",
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: null } },
providerMetadata: { "openai-responses": { itemId: "rs_1", reasoningEncryptedContent: null } },
})
}),
)
@@ -2829,20 +2829,20 @@ describe("OpenAI Responses route", () => {
{
type: "reasoning-start",
id: "rs_1:0",
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: null } },
providerMetadata: { "openai-responses": { itemId: "rs_1", reasoningEncryptedContent: null } },
},
{ type: "reasoning-delta", id: "rs_1:0", text: "First" },
{ type: "reasoning-end", id: "rs_1:0", providerMetadata: { openai: { itemId: "rs_1" } } },
{ type: "reasoning-end", id: "rs_1:0", providerMetadata: { "openai-responses": { itemId: "rs_1" } } },
{
type: "reasoning-start",
id: "rs_1:1",
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: null } },
providerMetadata: { "openai-responses": { itemId: "rs_1", reasoningEncryptedContent: null } },
},
{ type: "reasoning-delta", id: "rs_1:1", text: "Second" },
{
type: "reasoning-end",
id: "rs_1:1",
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
providerMetadata: { "openai-responses": { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
},
{ type: "step-finish", index: 0, reason: { normalized: "stop", raw: undefined } },
{ type: "finish", reason: { normalized: "stop", raw: undefined } },
@@ -2887,31 +2887,31 @@ describe("OpenAI Responses route", () => {
{
type: "reasoning-start",
id: "rs_1:0",
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: null } },
providerMetadata: { "openai-responses": { itemId: "rs_1", reasoningEncryptedContent: null } },
},
{ type: "reasoning-delta", id: "rs_1:0", text: "First", providerMetadata: undefined },
{ type: "reasoning-end", id: "rs_1:0", providerMetadata: { openai: { itemId: "rs_1" } } },
{ type: "reasoning-end", id: "rs_1:0", providerMetadata: { "openai-responses": { itemId: "rs_1" } } },
{
type: "reasoning-start",
id: "rs_1:1",
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: null } },
providerMetadata: { "openai-responses": { itemId: "rs_1", reasoningEncryptedContent: null } },
},
{ type: "reasoning-delta", id: "rs_1:1", text: "Second", providerMetadata: undefined },
{
type: "reasoning-end",
id: "rs_1:1",
providerMetadata: { openai: { itemId: "rs_1" } },
providerMetadata: { "openai-responses": { itemId: "rs_1" } },
},
{
type: "reasoning-start",
id: "rs_1:2",
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: null } },
providerMetadata: { "openai-responses": { itemId: "rs_1", reasoningEncryptedContent: null } },
},
{ type: "reasoning-delta", id: "rs_1:2", text: "Third", providerMetadata: undefined },
{
type: "reasoning-end",
id: "rs_1:2",
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
providerMetadata: { "openai-responses": { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
},
])
}),
@@ -3093,14 +3093,14 @@ describe("OpenAI Responses route", () => {
{
type: "reasoning-start",
id: "rs_1:0",
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: null } },
providerMetadata: { "openai-responses": { itemId: "rs_1", reasoningEncryptedContent: null } },
},
{ type: "reasoning-delta", id: "rs_1:0", text: "Checked the diff.", providerMetadata: undefined },
{
type: "reasoning-end",
id: "rs_1:0",
text: "Checked the diff.",
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
providerMetadata: { "openai-responses": { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
},
])
@@ -3178,11 +3178,11 @@ describe("OpenAI Responses route", () => {
)
expect(response.events.filter((event) => event.type === "reasoning-end")).toEqual([
{ type: "reasoning-end", id: "rs_1:0", providerMetadata: { openai: { itemId: "rs_1" } } },
{ type: "reasoning-end", id: "rs_1:0", providerMetadata: { "openai-responses": { itemId: "rs_1" } } },
{
type: "reasoning-end",
id: "rs_1:1",
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
providerMetadata: { "openai-responses": { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
},
])
}),
@@ -3201,7 +3201,7 @@ describe("OpenAI Responses route", () => {
type: "reasoning",
text: "Checked the previous diff.",
providerMetadata: {
openai: {
"openai-responses": {
itemId: "rs_1",
reasoningEncryptedContent: "encrypted-state",
},
@@ -3262,7 +3262,7 @@ describe("OpenAI Responses route", () => {
type: "reasoning",
text: "Checked order.",
providerMetadata: {
openai: {
"openai-responses": {
itemId: "rs_1",
reasoningEncryptedContent: "encrypted-state",
},
@@ -3298,7 +3298,9 @@ describe("OpenAI Responses route", () => {
{
type: "reasoning",
text: "Checked the previous diff.",
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
providerMetadata: {
"openai-responses": { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" },
},
},
]),
],
@@ -3330,7 +3332,7 @@ describe("OpenAI Responses route", () => {
name: "web_search",
input: { query: "effect 4" },
providerExecuted: true,
providerMetadata: { openai: { itemId: "ws_1" } },
providerMetadata: { "openai-responses": { itemId: "ws_1" } },
}),
{
type: "tool-result",
@@ -3338,7 +3340,7 @@ describe("OpenAI Responses route", () => {
name: "web_search",
result: { type: "json", value: item },
providerExecuted: true,
providerMetadata: { openai: { itemId: "ws_1" } },
providerMetadata: { "openai-responses": { itemId: "ws_1" } },
},
]),
Message.user("Continue."),
@@ -3368,7 +3370,7 @@ describe("OpenAI Responses route", () => {
name: "web_search",
input: { query: "effect 4" },
providerExecuted: true,
providerMetadata: { openai: { itemId: "ws_1" } },
providerMetadata: { "openai-responses": { itemId: "ws_1" } },
}),
{
type: "tool-result",
@@ -3376,7 +3378,7 @@ describe("OpenAI Responses route", () => {
name: "web_search",
result: { type: "json", value: item },
providerExecuted: true,
providerMetadata: { openai: { itemId: "ws_1" } },
providerMetadata: { "openai-responses": { itemId: "ws_1" } },
},
]),
Message.user("Continue."),
@@ -3410,7 +3412,7 @@ describe("OpenAI Responses route", () => {
name: item.type,
result: { type: "json", value: item },
providerExecuted: true,
providerMetadata: { openai: { itemId: item.id } },
providerMetadata: { "openai-responses": { itemId: item.id } },
}),
),
}),
@@ -3437,7 +3439,7 @@ describe("OpenAI Responses route", () => {
name: "web_search",
input: { query: "effect 4" },
providerExecuted: true,
providerMetadata: { openai: { itemId: "ws_1" } },
providerMetadata: { "openai-responses": { itemId: "ws_1" } },
}),
{
type: "tool-result",
@@ -3445,7 +3447,7 @@ describe("OpenAI Responses route", () => {
name: "web_search",
result: { type: "json", value: item },
providerExecuted: true,
providerMetadata: { openai: { itemId: "ws_1" } },
providerMetadata: { "openai-responses": { itemId: "ws_1" } },
},
]),
Message.user("Continue."),
@@ -3477,7 +3479,7 @@ describe("OpenAI Responses route", () => {
name: "web_search",
result: { type: "json", value: { type: "web_search_call", id: "ws_other", status: "completed" } },
providerExecuted: true,
providerMetadata: { openai: { itemId: "ws_1" } },
providerMetadata: { "openai-responses": { itemId: "ws_1" } },
},
]),
],
@@ -3503,39 +3505,41 @@ describe("OpenAI Responses route", () => {
{
type: "text",
text: "Hello",
providerMetadata: { openai: { itemId: "history_1" } },
providerMetadata: { "openai-responses": { itemId: "history_1" } },
},
{
type: "text",
text: "World",
providerMetadata: { openai: { itemId: `message_${"a".repeat(64)}` } },
providerMetadata: { "openai-responses": { itemId: `message_${"a".repeat(64)}` } },
},
{
type: "reasoning",
text: "Checked the diff.",
providerMetadata: { openai: { itemId: "thinking_1", reasoningEncryptedContent: "encrypted-state" } },
providerMetadata: {
"openai-responses": { itemId: "thinking_1", reasoningEncryptedContent: "encrypted-state" },
},
},
{
type: "reasoning",
text: "Missing suffix.",
providerMetadata: { openai: { itemId: "rs_", reasoningEncryptedContent: "another-state" } },
providerMetadata: { "openai-responses": { itemId: "rs_", reasoningEncryptedContent: "another-state" } },
},
{
type: "reasoning",
text: "No prefix separator.",
providerMetadata: { openai: { itemId: "550e8400-e29b-41d4-a716-446655440000" } },
providerMetadata: { "openai-responses": { itemId: "550e8400-e29b-41d4-a716-446655440000" } },
},
ToolCallPart.make({
id: "call_1",
name: "lookup",
input: { query: "weather" },
providerMetadata: { openai: { itemId: "toolu_01A" } },
providerMetadata: { "openai-responses": { itemId: "toolu_01A" } },
}),
ToolCallPart.make({
id: "call_2",
name: "lookup",
input: { query: "news" },
providerMetadata: { openai: { itemId: "fc_" } },
providerMetadata: { "openai-responses": { itemId: "fc_" } },
}),
]),
],
@@ -3597,7 +3601,7 @@ describe("OpenAI Responses route", () => {
name: "web_search",
input: { query: "effect 4" },
providerExecuted: true,
providerMetadata: { openai: { itemId } },
providerMetadata: { "openai-responses": { itemId } },
}),
{
type: "tool-result" as const,
@@ -3605,7 +3609,7 @@ describe("OpenAI Responses route", () => {
name: "web_search",
result: { type: "json" as const, value: item },
providerExecuted: true as const,
providerMetadata: { openai: { itemId } },
providerMetadata: { "openai-responses": { itemId } },
},
]
}
@@ -3641,7 +3645,7 @@ describe("OpenAI Responses route", () => {
name: "image_generation",
input: {},
providerExecuted: true,
providerMetadata: { openai: { itemId: "ig_1" } },
providerMetadata: { "openai-responses": { itemId: "ig_1" } },
}),
ToolResultPart.make({
id: "ig_1",
@@ -3651,7 +3655,7 @@ describe("OpenAI Responses route", () => {
value: [{ type: "file", uri: "data:image/png;base64,AQID", mime: "image/png" }],
},
providerExecuted: true,
providerMetadata: { openai: { itemId: "ig_1" } },
providerMetadata: { "openai-responses": { itemId: "ig_1" } },
}),
]),
Message.user("Make it blue."),
@@ -3682,7 +3686,7 @@ describe("OpenAI Responses route", () => {
name: "image_generation",
input: {},
providerExecuted: true,
providerMetadata: { openai: { itemId: "ig_1" } },
providerMetadata: { "openai-responses": { itemId: "ig_1" } },
}),
ToolResultPart.make({
id: "ig_1",
@@ -3692,7 +3696,7 @@ describe("OpenAI Responses route", () => {
value: [{ type: "file", uri: "data:image/png;base64,AQID", mime: "image/png" }],
},
providerExecuted: true,
providerMetadata: { openai: { itemId: "ig_1" } },
providerMetadata: { "openai-responses": { itemId: "ig_1" } },
}),
]),
],
@@ -3717,12 +3721,14 @@ describe("OpenAI Responses route", () => {
{
type: "reasoning",
text: "First",
providerMetadata: { openai: { itemId: "rs_1" } },
providerMetadata: { "openai-responses": { itemId: "rs_1" } },
},
{
type: "reasoning",
text: "Second",
providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
providerMetadata: {
"openai-responses": { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" },
},
},
]),
],
@@ -3757,7 +3763,7 @@ describe("OpenAI Responses route", () => {
type: "reasoning",
text: "Checked the previous diff.",
providerMetadata: {
openai: {
"openai-responses": {
itemId: "rs_1",
reasoningEncryptedContent: null,
},
@@ -3821,7 +3827,7 @@ describe("OpenAI Responses route", () => {
cacheReadInputTokens: undefined,
reasoningTokens: undefined,
totalTokens: 6,
providerMetadata: { openai: { input_tokens: 5, output_tokens: 1 } },
providerMetadata: { "openai-responses": { input_tokens: 5, output_tokens: 1 } },
})
expect(response.events).toEqual([
@@ -3830,7 +3836,7 @@ describe("OpenAI Responses route", () => {
type: "tool-input-start",
id: "call_1",
name: "lookup",
providerMetadata: { openai: { itemId: "fc_item_1" } },
providerMetadata: { "openai-responses": { itemId: "fc_item_1" } },
},
{
type: "tool-input-delta",
@@ -3850,7 +3856,7 @@ describe("OpenAI Responses route", () => {
type: "tool-input-end",
id: "call_1",
name: "lookup",
providerMetadata: { openai: { itemId: "fc_item_1" } },
providerMetadata: { "openai-responses": { itemId: "fc_item_1" } },
},
{
type: "tool-call",
@@ -3858,7 +3864,7 @@ describe("OpenAI Responses route", () => {
name: "lookup",
input: { query: "weather" },
providerExecuted: undefined,
providerMetadata: { openai: { itemId: "fc_item_1" } },
providerMetadata: { "openai-responses": { itemId: "fc_item_1" } },
},
{
type: "step-finish",
@@ -3927,7 +3933,7 @@ describe("OpenAI Responses route", () => {
name: "lookup",
input: { query: "weather" },
providerExecuted: undefined,
providerMetadata: { openai: { itemId: "fc_item_1" } },
providerMetadata: { "openai-responses": { itemId: "fc_item_1" } },
},
])
}),
@@ -4104,7 +4110,7 @@ describe("OpenAI Responses route", () => {
expect(response.events.find(LLMEvent.is.toolCall)).toMatchObject({
id: "call_1",
input: { query: "weather" },
providerMetadata: { openai: { itemId: "fc_item_1" } },
providerMetadata: { "openai-responses": { itemId: "fc_item_1" } },
})
expect(response.events.filter(LLMEvent.is.toolInputEnd)).toHaveLength(1)
expect(response.events.filter(LLMEvent.is.toolCall)).toHaveLength(1)
@@ -4228,7 +4234,7 @@ describe("OpenAI Responses route", () => {
type: "tool-input-end",
id: "call_1",
name: "lookup",
providerMetadata: { openai: { itemId: "fc_item_1" } },
providerMetadata: { "openai-responses": { itemId: "fc_item_1" } },
},
{
type: "tool-call",
@@ -4236,7 +4242,7 @@ describe("OpenAI Responses route", () => {
name: "lookup",
input: {},
providerExecuted: undefined,
providerMetadata: { openai: { itemId: "fc_item_1" } },
providerMetadata: { "openai-responses": { itemId: "fc_item_1" } },
},
],
)
@@ -4330,7 +4336,7 @@ describe("OpenAI Responses route", () => {
expect(response.events.find(LLMEvent.is.toolCall)).toMatchObject({
id: "call_1",
providerMetadata: { openai: { itemId: "fc_item_1" } },
providerMetadata: { "openai-responses": { itemId: "fc_item_1" } },
})
}),
)
@@ -4360,7 +4366,7 @@ describe("OpenAI Responses route", () => {
name: "web_search",
input: { type: "search", query: "effect 4" },
providerExecuted: true,
providerMetadata: { openai: { itemId: "ws_1" } },
providerMetadata: { "openai-responses": { itemId: "ws_1" } },
},
{
type: "tool-result",
@@ -4368,7 +4374,7 @@ describe("OpenAI Responses route", () => {
name: "web_search",
result: { type: "json", value: item },
providerExecuted: true,
providerMetadata: { openai: { itemId: "ws_1" } },
providerMetadata: { "openai-responses": { itemId: "ws_1" } },
},
])
}),
@@ -4401,7 +4407,7 @@ describe("OpenAI Responses route", () => {
name: "computer_use",
input: { type: "click", x: 100, y: 200 },
providerExecuted: true,
providerMetadata: { openai: { itemId: "computer_1" } },
providerMetadata: { "openai-responses": { itemId: "computer_1" } },
},
{
type: "tool-result",
@@ -4409,7 +4415,7 @@ describe("OpenAI Responses route", () => {
name: "computer_use",
result: { type: "json", value: item },
providerExecuted: true,
providerMetadata: { openai: { itemId: "computer_1" } },
providerMetadata: { "openai-responses": { itemId: "computer_1" } },
},
])
}),
@@ -4437,7 +4443,7 @@ describe("OpenAI Responses route", () => {
)
expect(response.events.find(LLMEvent.is.toolCall)).toMatchObject({
providerMetadata: { openai: { itemId: "ig_1" } },
providerMetadata: { "openai-responses": { itemId: "ig_1" } },
})
expect(response.events.find(LLMEvent.is.toolResult)).toMatchObject({
id: "ig_1",
@@ -4447,7 +4453,7 @@ describe("OpenAI Responses route", () => {
type: "content",
value: [{ type: "file", uri: "data:image/png;base64,AQID", mime: "image/png" }],
},
providerMetadata: { openai: { itemId: "ig_1" } },
providerMetadata: { "openai-responses": { itemId: "ig_1" } },
})
const prepared = yield* Effect.forEach([false, true], (store) =>
@@ -4481,7 +4487,7 @@ describe("OpenAI Responses route", () => {
expect(response.events.find(LLMEvent.is.toolResult)).toMatchObject({
result: { type: "error", value: item.error },
providerMetadata: { openai: { itemId: "ws_failed" } },
providerMetadata: { "openai-responses": { itemId: "ws_failed" } },
})
const prepared = yield* compileRequest(
@@ -4541,7 +4547,7 @@ describe("OpenAI Responses route", () => {
name: "code_interpreter",
input: { code: "print(1+1)", container_id: "cnt_xyz" },
providerExecuted: true,
providerMetadata: { openai: { itemId: "ci_1" } },
providerMetadata: { "openai-responses": { itemId: "ci_1" } },
})
const toolResult = response.events.find((event) => event.type === "tool-result")
expect(toolResult).toEqual({
@@ -4550,7 +4556,7 @@ describe("OpenAI Responses route", () => {
name: "code_interpreter",
result: { type: "json", value: item },
providerExecuted: true,
providerMetadata: { openai: { itemId: "ci_1" } },
providerMetadata: { "openai-responses": { itemId: "ci_1" } },
})
}),
)
@@ -65,7 +65,7 @@ describe("xAI Responses route", () => {
expect(response.message.content.find((part) => part.type === "reasoning")).toMatchObject({
type: "reasoning",
text: "Considering.",
providerMetadata: { xai: { itemId: "reasoning_1", reasoningEncryptedContent: "opaque" } },
providerMetadata: { "xai-responses": { itemId: "reasoning_1", reasoningEncryptedContent: "opaque" } },
})
}),
)
@@ -101,7 +101,7 @@ describe("xAI Responses route", () => {
expect(response.reasoning).toBe("Considering.")
expect(response.message.content.find((part) => part.type === "reasoning")).toMatchObject({
providerMetadata: { xai: { itemId: "reasoning_1", reasoningEncryptedContent: "opaque" } },
providerMetadata: { "xai-responses": { itemId: "reasoning_1", reasoningEncryptedContent: "opaque" } },
})
}),
)
@@ -120,7 +120,7 @@ describe("xAI Responses route", () => {
name: "x_search",
result: { type: "json", value: item },
providerExecuted: true,
providerMetadata: { xai: { itemId: "x_search_1" } },
providerMetadata: { "xai-responses": { itemId: "x_search_1" } },
},
]),
],
@@ -148,7 +148,7 @@ describe("xAI Responses route", () => {
name: item.type,
result: { type: "json", value: item },
providerExecuted: true,
providerMetadata: { xai: { itemId: item.id } },
providerMetadata: { "xai-responses": { itemId: item.id } },
}),
),
}),
@@ -181,11 +181,11 @@ describe("xAI Responses route", () => {
name: "x_search",
input: { query: "news" },
providerExecuted: true,
providerMetadata: { xai: { itemId: "x_search_1" } },
providerMetadata: { "xai-responses": { itemId: "x_search_1" } },
})
expect(response.events.find(LLMEvent.is.toolResult)).toMatchObject({
result: { type: "json", value: item },
providerMetadata: { xai: { itemId: "x_search_1" } },
providerMetadata: { "xai-responses": { itemId: "x_search_1" } },
})
}),
)
+2 -2
View File
@@ -253,10 +253,10 @@ const assistantMessageFromResponse = (response: LLMResponse, step: AssistantStep
if (step.reasoning === "openai-encrypted") {
const reasoning = response.events.find(
(event): event is Extract<LLMEvent, { readonly type: "reasoning-end" }> =>
LLMEvent.is.reasoningEnd(event) && typeof event.providerMetadata?.openai?.itemId === "string",
LLMEvent.is.reasoningEnd(event) && typeof event.providerMetadata?.["openai-responses"]?.itemId === "string",
)
if (!reasoning) throw new Error("OpenAI Responses did not return reasoning metadata")
expect(reasoning.providerMetadata?.openai?.reasoningEncryptedContent).toEqual(expect.any(String))
expect(reasoning.providerMetadata?.["openai-responses"]?.reasoningEncryptedContent).toEqual(expect.any(String))
content.push({ type: "reasoning", text: response.reasoning, providerMetadata: reasoning.providerMetadata })
}
+23 -8
View File
@@ -47,14 +47,29 @@ describe("Route.with", () => {
})
})
test("assigns metadata ownership to a replacement provider and preserves explicit overrides", () => {
const route = OpenAIChat.route.with({ provider: "azure" })
const overridden = route.with({ providerMetadataKey: "custom-azure" }).with({ headers: { "x-test": "value" } })
expect(route.providerMetadataKey).toBe("azure")
expect(overridden.providerMetadataKey).toBe("custom-azure")
expect(overridden.defaults).not.toHaveProperty("providerMetadataKey")
})
it.effect("namespaces emitted metadata by route ID independently of provider identity", () =>
Effect.gen(function* () {
const base = OpenAIChat.route.with({ provider: "azure", auth: Auth.none })
for (const route of [base, base.with({ id: "custom-azure-chat" })]) {
const response = yield* LLMClient.generate(
LLM.request({ model: route.model({ id: "test" }), prompt: "Hello" }),
).pipe(
Effect.provide(
fixedResponse(
sseEvents({
choices: [{ delta: { content: "Hello" }, finish_reason: "stop" }],
usage: { prompt_tokens: 2, completion_tokens: 1, total_tokens: 3 },
}),
),
),
)
expect(response.usage?.providerMetadata).toEqual({
[route.id]: { prompt_tokens: 2, completion_tokens: 1, total_tokens: 3 },
})
expect(route).not.toHaveProperty("providerMetadataKey")
}
}),
)
})
describe("Route diagnostics", () => {
+42 -30
View File
@@ -309,7 +309,6 @@ function modelFromLanguage(info: Info, language: LanguageModelV3) {
compact: undefined,
id: `ai-sdk:${packageName}`,
provider: ProviderID.make(providerID),
providerMetadataKey: optionKey,
protocol: "ai-sdk",
endpoint: Endpoint.path("/", { baseURL: "https://ai-sdk.local" }),
auth: Auth.none,
@@ -342,7 +341,16 @@ function modelFromLanguage(info: Info, language: LanguageModelV3) {
model: (input) =>
LanguageModel.make({ ...input, provider: "provider" in input ? input.provider : providerID, route }),
prepareTransport: (body) => Effect.succeed(body),
streamPrepared: (prepared) => streamLanguage(language, prepared as LanguageModelV3CallOptions),
streamPrepared: (prepared) =>
streamLanguage(language, prepared as LanguageModelV3CallOptions).pipe(
Stream.map((event) => {
if (!("providerMetadata" in event)) return event
// Preserve the same inner state that Session persists, while keeping SDK
// option namespaces private to this bridge.
const state = event.providerMetadata?.[optionKey]
return { ...event, providerMetadata: state === undefined ? undefined : { [route.id]: state } }
}),
),
}
return LanguageModel.make({
id: info.modelID ?? info.id,
@@ -418,7 +426,7 @@ function callOptions(
): LanguageModelV3CallOptions {
const flattened = ProviderShared.flattenToolRequest(request)
return {
prompt: prompt(flattened.request),
prompt: prompt(flattened.request, optionKey),
maxOutputTokens: request.generation?.maxTokens,
temperature: request.generation?.temperature,
stopSequences: request.generation?.stop === undefined ? undefined : [...request.generation.stop],
@@ -434,15 +442,21 @@ function callOptions(
}
}
function prompt(request: LLMRequest): LanguageModelV3Prompt {
type MetadataOptions = (input: ProviderMetadata | undefined) => SharedV3ProviderOptions | undefined
function prompt(request: LLMRequest, optionKey: string): LanguageModelV3Prompt {
const metadata: MetadataOptions = (input) => {
const state = input?.[request.model.route.id]
return state === undefined ? undefined : { [optionKey]: jsonObject(state) }
}
const system = request.system
.map((part) => part.text)
.filter(Boolean)
.join("\n\n")
const pending: UserContent = []
const messages = request.messages.flatMap((input, index) => {
if (input.role !== "tool") return message(input)
const lowered = toolMessage(input)
if (input.role !== "tool") return message(input, metadata)
const lowered = toolMessage(input, metadata)
pending.push(...lowered.media)
if (request.messages[index + 1]?.role === "tool" || pending.length === 0) return lowered.messages
const media = [...pending]
@@ -459,7 +473,7 @@ function prompt(request: LLMRequest): LanguageModelV3Prompt {
return [{ role: "system", content: system }, ...messages]
}
function message(input: LLMRequest["messages"][number]): LanguageModelV3Message[] {
function message(input: LLMRequest["messages"][number], metadata: MetadataOptions): LanguageModelV3Message[] {
switch (input.role) {
case "system":
// The initial privileged prompt lives in `request.system` and is prepended above. A system message here is a
@@ -479,29 +493,32 @@ function message(input: LLMRequest["messages"][number]): LanguageModelV3Message[
case "user":
return [{ role: "user", content: input.content.flatMap(userPart) }]
case "assistant":
return [{ role: "assistant", content: input.content.flatMap(assistantPart) }]
return [{ role: "assistant", content: input.content.flatMap((part) => assistantPart(part, metadata)) }]
case "tool":
return toolMessage(input).messages
return toolMessage(input, metadata).messages
}
}
function toolMessage(input: LLMRequest["messages"][number]) {
function toolMessage(input: LLMRequest["messages"][number], metadata: MetadataOptions) {
const media: UserContent = []
const content = input.content.flatMap((part) => {
if (part.type !== "tool-result" || part.result.type !== "content") return toolResultPart(part)
if (part.type !== "tool-result" || part.result.type !== "content") return toolResultPart(part, metadata)
const value = part.result.value.filter((item) => {
if (item.type !== "file") return true
if (!item.mime.startsWith("image/") && item.mime !== "application/pdf") return true
media.push({ type: "file", mediaType: item.mime, data: fileData(item.uri), filename: item.name })
return false
})
return toolResultPart({
...part,
result:
value.length === 0
? { type: "text", value: "Media attached in the following user message." }
: { ...part.result, value },
})
return toolResultPart(
{
...part,
result:
value.length === 0
? { type: "text", value: "Media attached in the following user message." }
: { ...part.result, value },
},
metadata,
)
})
return {
messages: content.length ? ([{ role: "tool", content }] satisfies LanguageModelV3Message[]) : [],
@@ -520,7 +537,7 @@ function userPart(part: ContentPart): UserContent {
return []
}
function assistantPart(part: ContentPart): AssistantContent {
function assistantPart(part: ContentPart, metadata: MetadataOptions): AssistantContent {
switch (part.type) {
case "compaction":
throw ProviderShared.unsupportedOperation({
@@ -529,11 +546,11 @@ function assistantPart(part: ContentPart): AssistantContent {
message: "AI SDK routes cannot replay native provider compaction state",
})
case "text":
return [{ type: "text", text: part.text, providerOptions: metadataProviderOptions(part.providerMetadata) }]
return [{ type: "text", text: part.text, providerOptions: metadata(part.providerMetadata) }]
case "media":
return [{ type: "file", mediaType: part.mediaType, data: fileData(part.data), filename: part.filename }]
case "reasoning":
return [{ type: "reasoning", text: part.text, providerOptions: metadataProviderOptions(part.providerMetadata) }]
return [{ type: "reasoning", text: part.text, providerOptions: metadata(part.providerMetadata) }]
case "tool-call":
return [
{
@@ -542,11 +559,11 @@ function assistantPart(part: ContentPart): AssistantContent {
toolName: part.name,
input: part.input,
providerExecuted: part.providerExecuted,
providerOptions: metadataProviderOptions(part.providerMetadata),
providerOptions: metadata(part.providerMetadata),
},
]
case "tool-result":
return toolResultPart(part)
return toolResultPart(part, metadata)
}
}
@@ -559,7 +576,7 @@ function fileData(data: Extract<ContentPart, { type: "media" }>["data"]) {
return url.protocol === "http:" || url.protocol === "https:" ? url : data
}
function toolResultPart(part: ContentPart): ToolResultContent[] {
function toolResultPart(part: ContentPart, metadata: MetadataOptions): ToolResultContent[] {
if (part.type !== "tool-result") return []
return [
{
@@ -567,7 +584,7 @@ function toolResultPart(part: ContentPart): ToolResultContent[] {
toolCallId: part.id,
toolName: part.name,
output: toolOutput(part.result),
providerOptions: metadataProviderOptions(part.providerMetadata),
providerOptions: metadata(part.providerMetadata),
},
]
}
@@ -624,11 +641,6 @@ function requestProviderOptions(
return { [optionKey]: options }
}
function metadataProviderOptions(input: ProviderMetadata | undefined): SharedV3ProviderOptions | undefined {
if (!input) return undefined
return Object.fromEntries(Object.entries(input).map(([key, value]) => [key, jsonObject(value)]))
}
function streamLanguage(language: LanguageModelV3, options: LanguageModelV3CallOptions) {
const state = { step: 0, toolNames: {} as Record<string, string> }
return Stream.concat(
+2 -3
View File
@@ -147,7 +147,7 @@ export const estimateTokens = (input: RequiredInput) => {
const last = input.messages[index]
// Keep the anchor's local tool results: they are not covered by its provider usage.
const added = SessionModelRequest.unsupportedParts(
toLLMMessages(input.messages.slice(Math.max(0, index)), input.resolved.ref),
toLLMMessages(input.messages.slice(Math.max(0, index)), input.resolved.ref, input.resolved.model.route.id),
input.resolved.capabilities,
)
.filter((message) => message.role !== "assistant" || message.id !== last?.id)
@@ -450,8 +450,7 @@ export const layer = Layer.effect(
})
}
if (LLMEvent.is.stepFinish(event)) {
providerState =
event.providerMetadata?.[context.model.model.route.providerMetadataKey ?? context.model.model.provider]
providerState = event.providerMetadata?.[context.model.model.route.id]
const step = SessionUsage.record(event.usage, context.model.cost)
usage = usage ? SessionUsage.add(usage, step) : step
}
+1 -3
View File
@@ -92,9 +92,7 @@ export const baseTranscript = (input: {
readonly initial: string
readonly messages: ReadonlyArray<SessionMessage.Info>
}) => {
const providerMetadataKey = input.model.model.route.providerMetadataKey ?? input.model.model.provider
return {
providerMetadataKey,
system: [
input.agent.system
? input.agent.system
@@ -103,7 +101,7 @@ export const baseTranscript = (input: {
]
.filter((part) => part.length > 0)
.map(SystemPart.make),
messages: toLLMMessages(input.messages, input.model.ref, providerMetadataKey),
messages: toLLMMessages(input.messages, input.model.ref, input.model.model.route.id),
}
}
@@ -18,7 +18,7 @@ type Input = {
readonly sessionID: SessionSchema.ID
readonly agent: Agent.ID
readonly model: Model.Ref
readonly providerMetadataKey: string
readonly routeID: string
readonly snapshot?: Snapshot.ID
readonly assistantMessageID: SessionMessage.ID
}
@@ -115,7 +115,7 @@ export const createLLMEventPublisher = (bus: Pick<Bus.Interface, "publish">, inp
assistantMessageID: yield* startAssistant(),
})
})
const providerState = (metadata: ProviderMetadata | undefined) => metadata?.[input.providerMetadataKey]
const providerState = (metadata: ProviderMetadata | undefined) => metadata?.[input.routeID]
const fragments = (
name: string,
ended: (id: string, value: string, ordinal: number, state?: Record<string, unknown>) => Effect.Effect<void>,
+1 -1
View File
@@ -75,7 +75,7 @@ export const make = Effect.gen(function* () {
assistantMessageID: input.assistantMessageID,
agent: input.agent,
model: input.model.ref,
providerMetadataKey: input.model.model.route.providerMetadataKey ?? input.model.model.provider,
routeID: input.model.model.route.id,
snapshot: startSnapshot,
})
const toolRuns: Array<{
@@ -95,10 +95,8 @@ const userAttachmentContent = (files: readonly FileAttachment[]) => {
const decodeToolInput = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Unknown))
const providerMetadata = (
provider: string,
state: Record<string, unknown> | undefined,
): ProviderMetadata | undefined => (state === undefined ? undefined : { [provider]: state })
const providerMetadata = (routeID: string, state: Record<string, unknown> | undefined): ProviderMetadata | undefined =>
state === undefined ? undefined : { [routeID]: state }
const toolInput = (tool: SessionMessage.AssistantTool) =>
tool.state.status === "streaming"
@@ -142,7 +140,7 @@ const toolResult = (tool: SessionMessage.AssistantTool, providerMetadata: Provid
}
}
const assistant = (message: SessionMessage.Assistant, model: Model.Ref, providerMetadataKey: string) => {
const assistant = (message: SessionMessage.Assistant, model: Model.Ref, routeID: string) => {
const sameProvider = String(message.model.providerID) === String(model.providerID)
const sameModel = sameProvider && String(message.model.id) === String(model.id)
const reuseProviderMetadata = sameModel && message.error === undefined
@@ -154,7 +152,7 @@ const assistant = (message: SessionMessage.Assistant, model: Model.Ref, provider
text: item.text,
// Text can carry provider-bound state (e.g. Gemini thought signatures),
// which is only replayable against the model that produced it.
providerMetadata: reuseProviderMetadata ? providerMetadata(providerMetadataKey, item.state) : undefined,
providerMetadata: reuseProviderMetadata ? providerMetadata(routeID, item.state) : undefined,
},
]
// Let the destination adapter handle readable reasoning after a model/provider switch.
@@ -164,7 +162,7 @@ const assistant = (message: SessionMessage.Assistant, model: Model.Ref, provider
{
type: "reasoning",
text: item.text,
providerMetadata: providerMetadata(providerMetadataKey, item.state),
providerMetadata: providerMetadata(routeID, item.state),
},
]
: item.text.length > 0
@@ -176,10 +174,7 @@ const assistant = (message: SessionMessage.Assistant, model: Model.Ref, provider
const reuseToolProviderMetadata =
reuseProviderMetadata ||
(sameModel && item.executed === true && (item.state.status === "completed" || item.state.status === "error"))
const call = toolCall(
item,
reuseToolProviderMetadata ? providerMetadata(providerMetadataKey, item.providerState) : undefined,
)
const call = toolCall(item, reuseToolProviderMetadata ? providerMetadata(routeID, item.providerState) : undefined)
if (item.executed !== true) return [call]
// Hosted tools (e.g. google_search) run inside the provider, so their
// result payload (`providerResultState`) is provider-format data rather
@@ -190,9 +185,9 @@ const assistant = (message: SessionMessage.Assistant, model: Model.Ref, provider
const result = toolResult(
item,
reuseToolProviderMetadata
? providerMetadata(providerMetadataKey, item.providerResultState ?? item.providerState)
? providerMetadata(routeID, item.providerResultState ?? item.providerState)
: sameProvider && item.providerResultState !== undefined
? providerMetadata(providerMetadataKey, item.providerResultState)
? providerMetadata(routeID, item.providerResultState)
: undefined,
)
return result ? [call, result] : [call]
@@ -207,9 +202,7 @@ const assistant = (message: SessionMessage.Assistant, model: Model.Ref, provider
.map((item) =>
toolResult(
item,
reuseProviderMetadata
? providerMetadata(providerMetadataKey, item.providerResultState ?? item.providerState)
: undefined,
reuseProviderMetadata ? providerMetadata(routeID, item.providerResultState ?? item.providerState) : undefined,
),
)
.filter((message) => message !== undefined)
@@ -221,7 +214,7 @@ const assistant = (message: SessionMessage.Assistant, model: Model.Ref, provider
]
}
function toLLMMessage(message: SessionMessage.Info, model: Model.Ref, providerMetadataKey: string): Message[] {
function toLLMMessage(message: SessionMessage.Info, model: Model.Ref, routeID: string): Message[] {
switch (message.type) {
case "agent-switched":
case "model-switched":
@@ -271,7 +264,7 @@ function toLLMMessage(message: SessionMessage.Info, model: Model.Ref, providerMe
}),
]
case "assistant":
return assistant(message, model, providerMetadataKey)
return assistant(message, model, routeID)
case "compaction":
if (message.status !== "completed") return []
return [
@@ -296,8 +289,5 @@ ${message.recent}
}
/** Translate projected Session history into canonical @opencode-ai/ai context. */
export const toLLMMessages = (
messages: readonly SessionMessage.Info[],
model: Model.Ref,
providerMetadataKey: string = model.providerID,
) => messages.flatMap((message) => toLLMMessage(message, model, providerMetadataKey))
export const toLLMMessages = (messages: readonly SessionMessage.Info[], model: Model.Ref, routeID: string) =>
messages.flatMap((message) => toLLMMessage(message, model, routeID))
+44 -5
View File
@@ -145,7 +145,7 @@ it.effect("uses canonical names and metadata without merging connection cache pa
const resolved = yield* aisdk.model(input)
expect(resolved).toMatchObject({ id: "api-model", provider: "openai" })
expect(resolved.route).toMatchObject({ provider: "openai", providerMetadataKey: "openai" })
expect(resolved.route).toMatchObject({ provider: "openai", id: "ai-sdk:@ai-sdk/openai-compatible" })
expect(resolved.route.model({ id: "another-model" })).toMatchObject({ provider: "openai" })
const prepared = yield* compileRequest(LLM.request({ model: resolved, prompt: "Hello" }))
expect(prepared.body.providerOptions).toEqual({ openai: { reasoningEffort: "high" } })
@@ -385,22 +385,26 @@ it.effect("projects replay metadata onto AI SDK prompt parts", () =>
})
const resolved = yield* aisdk.model(model("@ai-sdk/anthropic"))
expect(resolved.route.providerMetadataKey).toBe("anthropic")
expect(resolved.route.id).toBe("ai-sdk:@ai-sdk/anthropic")
const prepared = yield* compileRequest(
LLM.request({
model: resolved,
messages: [
Message.assistant([
{ type: "text", text: "Answer", providerMetadata: { anthropic: { cacheControl: { type: "ephemeral" } } } },
{
type: "text",
text: "Answer",
providerMetadata: { [resolved.route.id]: { cacheControl: { type: "ephemeral" } } },
},
{ type: "text", text: " without metadata" },
{ type: "reasoning", text: "Think", providerMetadata: { anthropic: { signature: "signed" } } },
{ type: "reasoning", text: "Think", providerMetadata: { [resolved.route.id]: { signature: "signed" } } },
{
type: "tool-call",
id: "hosted",
name: "web_search",
input: { query: "Effect" },
providerExecuted: true,
providerMetadata: { anthropic: { blockType: "server_tool_use" } },
providerMetadata: { [resolved.route.id]: { blockType: "server_tool_use" } },
},
]),
],
@@ -440,6 +444,41 @@ it.effect("projects replay metadata onto AI SDK prompt parts", () =>
}),
)
it.effect("round-trips SDK metadata through the route namespace", () =>
Effect.gen(function* () {
const aisdk = yield* AISDK.Service
yield* aisdk.hook.sdk((event) => {
event.sdk = {
languageModel: () =>
streamModel([
{ type: "reasoning-start", id: "thinking" },
{ type: "reasoning-delta", id: "thinking", delta: "Think" },
{ type: "reasoning-end", id: "thinking", providerMetadata: { anthropic: { signature: "signed" } } },
{ type: "finish", finishReason: { unified: "stop", raw: "end_turn" }, usage },
]),
}
})
const resolved = yield* aisdk.model(model("@ai-sdk/anthropic"))
const response = yield* LLMClient.generate(LLM.request({ model: resolved, prompt: "Think" })).pipe(
Effect.provide(client),
)
expect(response.message.content).toEqual([
{
type: "reasoning",
text: "Think",
providerMetadata: { "ai-sdk:@ai-sdk/anthropic": { signature: "signed" } },
},
])
const replay = yield* compileRequest(LLM.request({ model: resolved, messages: [response.message] }))
expect(replay.body.prompt).toEqual([
{
role: "assistant",
content: [{ type: "reasoning", text: "Think", providerOptions: { anthropic: { signature: "signed" } } }],
},
])
}),
)
it.effect("normalizes file data across AI SDK prompt parts", () =>
Effect.gen(function* () {
const aisdk = yield* AISDK.Service
+1 -4
View File
@@ -253,7 +253,6 @@ describe("ModelResolver", () => {
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" },
@@ -469,7 +468,6 @@ describe("ModelResolver", () => {
expect(headers.authorization).toBe("Bearer settings-secret")
expect(resolved.route.id).toBe("openai-compatible-chat")
expect(String(resolved.provider)).toBe("deepseek")
expect(resolved.route.providerMetadataKey).toBe("deepseek")
expect(resolved.compatibility?.reasoningField).toBe("vendor_reasoning")
expect(resolved.compatibility?.requireReasoning).toBe(true)
expect(resolved.compatibility?.maxTokensField).toBe("max_completion_tokens")
@@ -640,7 +638,6 @@ describe("ModelResolver", () => {
expect(resolved.route).toMatchObject({
id: "anthropic-messages",
providerMetadataKey: "anthropic",
endpoint: { baseURL: "https://anthropic.example/v1" },
})
}),
@@ -1262,7 +1259,7 @@ describe("ModelResolver", () => {
reasoningEffort: "high",
})
expect(String(mistral.provider)).toBe("test-provider")
expect(xai.route.id).toBe("openai-responses")
expect(xai.route.id).toBe("xai-responses")
expect(xai.route.defaults.providerOptions).toEqual({
reasoningEffort: "high",
store: false,
@@ -314,7 +314,7 @@ describe("GithubCopilotPlugin", () => {
loadAISDK: (model) => aisdk.model(model),
})
expect(resolved.route.id).toBe("ai-sdk:@ai-sdk/github-copilot")
expect(resolved.route.providerMetadataKey).toBe("copilot")
expect(resolved.route.id).toBe("ai-sdk:@ai-sdk/github-copilot")
}),
)
@@ -317,7 +317,7 @@ describe("SessionInstructions", () => {
metadata: { instruction: { paths: ["/repo/sub/AGENTS.md"] } },
time: { created },
})
const messages = toLLMMessages([synthetic], model)
const messages = toLLMMessages([synthetic], model, "test-route")
expect(messages).toHaveLength(1)
expect(messages[0]!.role).toBe("user")
expect(messages[0]!.content).toEqual([{ type: "text", text: "Instructions from: /repo/sub/AGENTS.md\ncontent" }])
@@ -38,7 +38,7 @@ describe("toLLMMessages", () => {
time: { created },
})
expect(toLLMMessages([shell], model)).toEqual([])
expect(toLLMMessages([shell], model, "test-route")).toEqual([])
const completed = SessionMessage.Shell.make({
...shell,
status: "exited",
@@ -46,8 +46,8 @@ describe("toLLMMessages", () => {
output: { output: "/project", cursor: 8, size: 8, truncated: false },
time: { created, completed: created },
})
expect(toLLMMessages([completed], model)).toEqual([])
expect(toLLMMessages([completed, notification], model)).toEqual([
expect(toLLMMessages([completed], model, "test-route")).toEqual([])
expect(toLLMMessages([completed, notification], model, "test-route")).toEqual([
Message.make({ id: notification.id, role: "user", content: notification.text }),
])
})
@@ -77,6 +77,7 @@ describe("toLLMMessages", () => {
]),
],
model,
"test-route",
)
expect(messages.map((message) => message.id)).toEqual([id("text"), id("reasoning")])
@@ -153,6 +154,7 @@ describe("toLLMMessages", () => {
}),
],
model,
"test-route",
)
expect(messages.map((message) => message.role)).toEqual(["user", "system", "user", "user", "user", "user"])
@@ -220,6 +222,7 @@ Recent work
}),
],
model,
"test-route",
)
expect(messages).toHaveLength(1)
@@ -259,6 +262,7 @@ Recent work
}),
],
model,
"test-route",
)
expect(messages).toEqual([
@@ -293,6 +297,7 @@ Recent work
}),
],
model,
"test-route",
)
expect(messages[0]).toMatchObject({
@@ -320,6 +325,7 @@ Recent work
}),
],
model,
"test-route",
)
expect(messages[0]?.content).toMatchObject([
@@ -350,6 +356,7 @@ Recent work
}),
],
model,
"test-route",
)
expect(messages).toHaveLength(1)
@@ -393,6 +400,7 @@ Recent work
}),
],
model,
"test-route",
)
expect(messages).toHaveLength(1)
@@ -423,6 +431,7 @@ Recent work
}),
],
model,
"test-route",
)
expect(messages).toHaveLength(1)
@@ -452,6 +461,7 @@ Recent work
}),
],
model,
"test-route",
)
expect(messages[0]?.content).toEqual([
@@ -482,6 +492,7 @@ Recent work
}),
],
model,
"test-route",
)
expect(messages[0]?.content).toEqual([
@@ -517,6 +528,7 @@ Recent work
}),
],
model,
"test-route",
)
expect(messages[0]?.content).toEqual([
@@ -555,6 +567,7 @@ Recent work
}),
],
model,
"test-route",
)
expect(messages[0]?.content).toEqual([
@@ -599,6 +612,7 @@ Recent work
}),
],
model,
"test-route",
)
expect(messages[0]?.content).toEqual([
@@ -655,6 +669,7 @@ Recent work
}),
],
model,
"test-route",
)
expect(messages[0]?.content.filter((part) => part.type === "media")).toHaveLength(4)
@@ -744,12 +759,13 @@ Recent work
}),
],
model,
"test-route",
)
expect(messages.map((message) => message.role)).toEqual(["assistant", "tool"])
expect(messages[0]?.content).toEqual([
{ type: "text", text: "Checking" },
{ type: "reasoning", text: "Think", providerMetadata: { provider: { signature: "sig_1" } } },
{ type: "reasoning", text: "Think", providerMetadata: { "test-route": { signature: "sig_1" } } },
{ type: "tool-call", id: "pending", name: "read", input: { path: "README.md" } },
{ type: "tool-call", id: "running", name: "read", input: { path: "README.md" } },
{
@@ -764,14 +780,14 @@ Recent work
name: "web_search",
input: { query: "Effect" },
providerExecuted: true,
providerMetadata: { provider: { continuation: "hosted-call" } },
providerMetadata: { "test-route": { continuation: "hosted-call" } },
},
{
type: "tool-result",
id: "hosted",
name: "web_search",
providerExecuted: true,
providerMetadata: { provider: { continuation: "hosted-result" } },
providerMetadata: { "test-route": { continuation: "hosted-result" } },
result: { type: "text", value: "Found it" },
},
{
@@ -780,14 +796,14 @@ Recent work
name: "write",
input: { path: "README.md" },
providerExecuted: true,
providerMetadata: { provider: { continuation: "failed" } },
providerMetadata: { "test-route": { continuation: "failed" } },
},
{
type: "tool-result",
id: "hosted-failed",
name: "write",
providerExecuted: true,
providerMetadata: { provider: { continuation: "failed" } },
providerMetadata: { "test-route": { continuation: "failed" } },
result: {
type: "error",
value: { error: { type: "unknown", message: "Denied" }, content: [] },
@@ -829,13 +845,14 @@ Recent work
}),
],
model,
"test-route",
)
expect(messages[0]?.content).toEqual([
{
type: "reasoning",
text: "Think",
providerMetadata: { provider: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
providerMetadata: { "test-route": { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
},
])
})
@@ -860,11 +877,11 @@ Recent work
}),
],
opencode,
"anthropic",
"anthropic-messages",
)
expect(messages[0]?.content).toEqual([
{ type: "reasoning", text: "Think", providerMetadata: { anthropic: { signature: "signed" } } },
{ type: "reasoning", text: "Think", providerMetadata: { "anthropic-messages": { signature: "signed" } } },
])
})
@@ -917,6 +934,7 @@ Recent work
}),
],
model,
"test-route",
)
expect(messages[0]?.content).toEqual([
@@ -927,7 +945,7 @@ Recent work
name: "web_search",
input: { query: "Effect" },
providerExecuted: true,
providerMetadata: { provider: { itemId: "call_completed" } },
providerMetadata: { "test-route": { itemId: "call_completed" } },
},
{
type: "tool-result",
@@ -937,7 +955,7 @@ Recent work
providerExecuted: true,
cache: undefined,
metadata: undefined,
providerMetadata: { provider: { itemId: "result_completed" } },
providerMetadata: { "test-route": { itemId: "result_completed" } },
},
{
type: "tool-call",
@@ -945,7 +963,7 @@ Recent work
name: "web_search",
input: { query: "Effect" },
providerExecuted: true,
providerMetadata: { provider: { itemId: "call_failed" } },
providerMetadata: { "test-route": { itemId: "call_failed" } },
},
{
type: "tool-result",
@@ -961,7 +979,7 @@ Recent work
providerExecuted: true,
cache: undefined,
metadata: undefined,
providerMetadata: { provider: { itemId: "result_failed" } },
providerMetadata: { "test-route": { itemId: "result_failed" } },
},
])
})
@@ -1013,6 +1031,7 @@ Recent work
}),
],
model,
"test-route",
)
expect(messages[0]?.content).toEqual([
@@ -1035,7 +1054,7 @@ Recent work
metadata: undefined,
// Hosted result payloads are provider-format state and must survive a
// model switch within the same provider for replay to stay valid.
providerMetadata: { provider: { itemId: "hosted-old-model" } },
providerMetadata: { "test-route": { itemId: "hosted-old-model" } },
},
{
type: "tool-call",
@@ -1079,13 +1098,14 @@ Recent work
}),
],
Model.Ref.make({ id: Model.ID.make("fast"), providerID: Provider.ID.make("provider") }),
"test-route",
)
expect(messages[0]?.content).toEqual([
{
type: "reasoning",
text: "Visible thought",
providerMetadata: { provider: { reasoningEncryptedContent: "encrypted" } },
providerMetadata: { "test-route": { reasoningEncryptedContent: "encrypted" } },
},
])
})
@@ -1110,6 +1130,7 @@ Recent work
}),
],
Model.Ref.make({ id: Model.ID.make("new"), providerID: Provider.ID.make("provider") }),
"test-route",
)
expect(messages[0]?.content).toEqual([
@@ -1140,13 +1161,14 @@ Recent work
}),
],
Model.Ref.make({ id: Model.ID.make("same"), providerID: Provider.ID.make("provider") }),
"test-route",
)
expect(messages[0]?.content).toEqual([
{
type: "text",
text: "Checking.",
providerMetadata: { provider: { phase: "commentary" } },
providerMetadata: { "test-route": { phase: "commentary" } },
},
])
})
@@ -29,7 +29,7 @@ const sessionID = Session.ID.make("ses_tool_event_test")
const base64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB"
const capture = (
providerMetadataKey = "anthropic",
routeID = "anthropic-messages",
options?: { readonly interruptProgress?: boolean; readonly beforeTextDelta?: Effect.Effect<void> },
) => {
const published: Array<{ readonly type: string; readonly data: unknown }> = []
@@ -59,7 +59,7 @@ const capture = (
id: Model.ID.make("model"),
providerID: Provider.ID.opencode,
},
providerMetadataKey,
routeID,
assistantMessageID: SessionMessage.ID.create(),
}),
}
@@ -114,7 +114,7 @@ test("provider-executed success derives content and retains provider result stat
LLMEvent.toolResult({
...hostedResult,
providerExecuted: true,
providerMetadata: { anthropic: { result: { type: "content", value: [] } } },
providerMetadata: { "anthropic-messages": { result: { type: "content", value: [] } } },
}),
),
)
@@ -162,7 +162,7 @@ testEffect(
assistantMessageID,
agent: Agent.defaultID,
model: { id: Model.ID.make("test-model"), providerID: Provider.ID.opencode },
providerMetadataKey: "openai",
routeID: "openai-responses",
},
)
yield* publisher.publish(LLMEvent.toolCall({ ...call, providerExecuted: true }))
@@ -213,7 +213,7 @@ testEffect(
)
test("interrupted progress metadata remains in the terminal failure snapshot", async () => {
const { published, publisher } = capture("anthropic", { interruptProgress: true })
const { published, publisher } = capture("anthropic-messages", { interruptProgress: true })
await Effect.runPromise(publisher.publish(call))
const exit = await Effect.runPromiseExit(publisher.progress(call.id, { phase: "visible" }))
expect(Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause)).toBe(true)
@@ -225,7 +225,7 @@ test("interrupted progress metadata remains in the terminal failure snapshot", a
})
test("interrupted subagent failures expose their existing child session to the model", async () => {
const { published, publisher } = capture("anthropic", { interruptProgress: true })
const { published, publisher } = capture("anthropic-messages", { interruptProgress: true })
const subagent = LLMEvent.toolCall({
id: "call-subagent",
name: "subagent",
@@ -267,7 +267,7 @@ test("local failure metadata completes the progress snapshot", async () => {
})
test("failure snapshot retains canonical progress above the default byte limit", async () => {
const { published, publisher } = capture("anthropic", { interruptProgress: true })
const { published, publisher } = capture("anthropic-messages", { interruptProgress: true })
await Effect.runPromise(publisher.publish(call))
const detail = "x".repeat(60 * 1024)
await Effect.runPromiseExit(publisher.progress(call.id, { detail }))
@@ -292,7 +292,7 @@ test("provider metadata is flattened using the route key", async () => {
const { published, publisher } = capture()
await Effect.runPromise(
publisher.publish(
LLMEvent.reasoningStart({ id: "reasoning", providerMetadata: { anthropic: { signature: "signed" } } }),
LLMEvent.reasoningStart({ id: "reasoning", providerMetadata: { "anthropic-messages": { signature: "signed" } } }),
),
)
@@ -305,7 +305,10 @@ 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", providerMetadata: { anthropic: { blockType: "thinking" } } }),
LLMEvent.reasoningStart({
id: "reasoning",
providerMetadata: { "anthropic-messages": { blockType: "thinking" } },
}),
),
)
await Effect.runPromise(
@@ -313,13 +316,16 @@ test("reasoning state from start, empty delta, and end is merged", async () => {
LLMEvent.reasoningDelta({
id: "reasoning",
text: "",
providerMetadata: { anthropic: { signature: "signed" }, gateway: { traceID: "trace" } },
providerMetadata: { "anthropic-messages": { signature: "signed" }, gateway: { traceID: "trace" } },
}),
),
)
await Effect.runPromise(
publisher.publish(
LLMEvent.reasoningEnd({ id: "reasoning", providerMetadata: { anthropic: { stopReason: "tool_use" } } }),
LLMEvent.reasoningEnd({
id: "reasoning",
providerMetadata: { "anthropic-messages": { stopReason: "tool_use" } },
}),
),
)
@@ -389,7 +395,7 @@ it.effect("retains new chunks and orders text-end behind an in-flight timer publ
Effect.gen(function* () {
const entered = yield* Deferred.make<void>()
const release = yield* Deferred.make<void>()
const { published, publisher } = capture("anthropic", {
const { published, publisher } = capture("anthropic-messages", {
beforeTextDelta: Deferred.succeed(entered, undefined).pipe(Effect.andThen(Deferred.await(release))),
})
yield* publisher.publish(LLMEvent.textStart({ id: "text" }))
@@ -481,7 +487,7 @@ test("tool input deltas are accumulated without being published", async () => {
})
test("provider-executed tool metadata is flattened using the route key", async () => {
const { published, publisher } = capture("openai")
const { published, publisher } = capture("openai-responses")
await Effect.runPromise(
publisher.publish(
LLMEvent.toolCall({
@@ -489,7 +495,7 @@ test("provider-executed tool metadata is flattened using the route key", async (
name: "web_search",
input: { query: "Effect" },
providerExecuted: true,
providerMetadata: { openai: { itemId: "call" } },
providerMetadata: { "openai-responses": { itemId: "call" } },
}),
),
)
@@ -500,7 +506,7 @@ test("provider-executed tool metadata is flattened using the route key", async (
name: "web_search",
result: { type: "json", value: { found: true } },
providerExecuted: true,
providerMetadata: { openai: { itemId: "result" } },
providerMetadata: { "openai-responses": { itemId: "result" } },
}),
),
)
@@ -556,7 +562,7 @@ test("content-filter finish retains failure evidence until step closeout", async
index: 0,
reason: { normalized: "content-filter", raw: "refusal" },
providerMetadata: {
anthropic: {
"anthropic-messages": {
stopDetails: { type: "refusal", category: "safety", explanation: "Blocked" },
},
},