mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-07 09:26:26 +00:00
Compare commits
1
Commits
beta
...
route-metadata
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f240fbb8a6 |
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`, {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 }),
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
})
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" } },
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" },
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user