mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-28 11:37:37 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4cec2c78a |
@@ -26,7 +26,7 @@ import {
|
||||
type ToolDefinition,
|
||||
type ToolResultPart,
|
||||
} from "../schema/index.js"
|
||||
import { JsonObject, knownString, optionalArray, optionalNull, ProviderShared } from "./shared.js"
|
||||
import { JsonObject, knownString, lenient, optionalArray, optionalNull, ProviderShared } from "./shared.js"
|
||||
import { classifyProviderFailure } from "../provider-error.js"
|
||||
import { effortUpdate, resolveEffortUpdates } from "../effort-updates.js"
|
||||
import * as Cache from "./utils/cache.js"
|
||||
@@ -510,19 +510,15 @@ const cacheControl = (breakpoints: Cache.Breakpoints, cache: CacheHint | undefin
|
||||
return Cache.ttlBucket(cache.ttlSeconds) === "1h" ? EPHEMERAL_1H : EPHEMERAL_5M
|
||||
}
|
||||
|
||||
const providerMetadata = (key: string, metadata: Record<string, unknown>): ProviderMetadata => ({ [key]: metadata })
|
||||
|
||||
const signatureFromMetadata = (metadata: ProviderMetadata | undefined, key: string): string | undefined => {
|
||||
const provider = metadata?.[key]
|
||||
if (!ProviderShared.isRecord(provider)) return undefined
|
||||
return typeof provider.signature === "string" ? provider.signature : undefined
|
||||
}
|
||||
|
||||
const redactedDataFromMetadata = (metadata: ProviderMetadata | undefined, key: string): string | undefined => {
|
||||
const provider = metadata?.[key]
|
||||
if (!ProviderShared.isRecord(provider)) return undefined
|
||||
return typeof provider.redactedData === "string" ? provider.redactedData : undefined
|
||||
}
|
||||
const AnthropicProviderMetadata = ProviderShared.providerMetadata(
|
||||
Schema.Struct({
|
||||
signature: lenient(Schema.String),
|
||||
redactedData: lenient(Schema.String),
|
||||
blockType: lenient(Schema.String),
|
||||
result: lenient(Schema.Unknown),
|
||||
stopSequence: lenient(Schema.String),
|
||||
}),
|
||||
)
|
||||
|
||||
const lowerTool = (breakpoints: Cache.Breakpoints, tool: ToolDefinition, inputSchema: JsonSchema): AnthropicTool => ({
|
||||
name: tool.name,
|
||||
@@ -590,7 +586,8 @@ const lowerServerToolResult = Effect.fn("AnthropicMessages.lowerServerToolResult
|
||||
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 =
|
||||
AnthropicProviderMetadata.read(part.providerMetadata, providerMetadataKey)?.result ?? part.result.value
|
||||
return {
|
||||
type: wireType,
|
||||
tool_use_id: scrubToolCallID(part.id),
|
||||
@@ -937,8 +934,9 @@ 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 metadata = AnthropicProviderMetadata.read(part.providerMetadata, providerMetadataKey)
|
||||
const signature = part.encrypted ?? metadata?.signature
|
||||
const redactedData = metadata?.redactedData
|
||||
if (signature === undefined && redactedData !== undefined) {
|
||||
content.push({ type: "redacted_thinking", data: redactedData })
|
||||
continue
|
||||
@@ -1220,7 +1218,10 @@ 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: AnthropicProviderMetadata.write(providerMetadataKey, {
|
||||
blockType: block.type,
|
||||
result: block.content,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1284,7 +1285,7 @@ const onContentBlockStart = (
|
||||
const metadata =
|
||||
block.signature === undefined
|
||||
? undefined
|
||||
: providerMetadata(state.providerMetadataKey, { signature: block.signature })
|
||||
: AnthropicProviderMetadata.write(state.providerMetadataKey, { signature: block.signature })
|
||||
const lifecycle = Lifecycle.reasoningStart(state.lifecycle, events, id, metadata)
|
||||
return [
|
||||
{
|
||||
@@ -1313,7 +1314,7 @@ const onContentBlockStart = (
|
||||
state.lifecycle,
|
||||
events,
|
||||
`reasoning-${event.index ?? 0}`,
|
||||
providerMetadata(state.providerMetadataKey, { redactedData: block.data }),
|
||||
AnthropicProviderMetadata.write(state.providerMetadataKey, { redactedData: block.data }),
|
||||
),
|
||||
},
|
||||
events,
|
||||
@@ -1421,7 +1422,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 : AnthropicProviderMetadata.write(state.providerMetadataKey, { signature }),
|
||||
)
|
||||
events.push(...resultEvents)
|
||||
const reasoningSignatures = { ...state.reasoningSignatures }
|
||||
@@ -1442,7 +1443,7 @@ const onMessageDelta = (
|
||||
const finishMetadata =
|
||||
stopSequence === null || stopSequence === undefined
|
||||
? state.pendingFinish?.providerMetadata
|
||||
: providerMetadata(state.providerMetadataKey, { stopSequence })
|
||||
: AnthropicProviderMetadata.write(state.providerMetadataKey, { stopSequence })
|
||||
return {
|
||||
reason: {
|
||||
normalized: mapFinishReason(stopReason),
|
||||
@@ -1474,7 +1475,7 @@ const onMessageStop = Effect.fn("AnthropicMessages.onMessageStop")(function* (st
|
||||
current,
|
||||
events,
|
||||
`reasoning-${index}`,
|
||||
providerMetadata(state.providerMetadataKey, { signature }),
|
||||
AnthropicProviderMetadata.write(state.providerMetadataKey, { signature }),
|
||||
),
|
||||
lifecycle,
|
||||
)
|
||||
|
||||
@@ -12,15 +12,13 @@ import {
|
||||
type JsonSchema,
|
||||
type LLMRequest,
|
||||
type LanguageModelToolSchemaCompatibility,
|
||||
type ProviderMetadata,
|
||||
type ReasoningPart,
|
||||
type ToolCallPart,
|
||||
type ToolDefinition,
|
||||
type ToolResultPart,
|
||||
} from "../schema/index.js"
|
||||
import { BedrockEventStream } from "./bedrock-event-stream.js"
|
||||
import { classifyProviderFailure } from "../provider-error.js"
|
||||
import { JsonObject, optionalArray, ProviderShared } from "./shared.js"
|
||||
import { JsonObject, lenient, optionalArray, ProviderShared } from "./shared.js"
|
||||
import { BedrockAuth } from "./utils/bedrock-auth.js"
|
||||
import { BedrockCache } from "./utils/bedrock-cache.js"
|
||||
import { BedrockMedia } from "./utils/bedrock-media.js"
|
||||
@@ -259,18 +257,12 @@ const lowerToolChoice = (toolChoice: NonNullable<LLMRequest["toolChoice"]>) =>
|
||||
tool: (name) => ({ tool: { name } }) as const,
|
||||
})
|
||||
|
||||
const providerMetadata = (key: string, metadata: Record<string, unknown>): ProviderMetadata => ({ [key]: metadata })
|
||||
|
||||
const reasoningSignature = (part: ReasoningPart, providerMetadataKey: string) => {
|
||||
const metadata = part.providerMetadata?.[providerMetadataKey]
|
||||
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]
|
||||
if (ProviderShared.isRecord(metadata) && typeof metadata.redactedData === "string") return metadata.redactedData
|
||||
}
|
||||
const BedrockProviderMetadata = ProviderShared.providerMetadata(
|
||||
Schema.Struct({
|
||||
signature: lenient(Schema.String),
|
||||
redactedData: lenient(Schema.String),
|
||||
}),
|
||||
)
|
||||
|
||||
const removeEmptyToolInputKeys = (input: unknown): unknown => {
|
||||
if (Array.isArray(input)) return input.map(removeEmptyToolInputKeys)
|
||||
@@ -387,8 +379,9 @@ const lowerMessages = Effect.fn("BedrockConverse.lowerMessages")(function* (
|
||||
continue
|
||||
}
|
||||
if (part.type === "reasoning") {
|
||||
const signature = reasoningSignature(part, providerMetadataKey)
|
||||
const redactedData = reasoningRedactedData(part, providerMetadataKey)
|
||||
const metadata = BedrockProviderMetadata.read(part.providerMetadata, providerMetadataKey)
|
||||
const signature = part.encrypted ?? metadata?.signature
|
||||
const redactedData = metadata?.redactedData
|
||||
if (signature === undefined && redactedData !== undefined) {
|
||||
content.push({ reasoningContent: { redactedContent: redactedData } })
|
||||
continue
|
||||
@@ -602,8 +595,10 @@ 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 BedrockProviderMetadata.write(state.providerMetadataKey, { signature: reasoning.signature })
|
||||
if (redactedData !== undefined)
|
||||
return BedrockProviderMetadata.write(state.providerMetadataKey, { redactedData })
|
||||
})()
|
||||
const lifecycle = (() => {
|
||||
if (reasoning.text === undefined && metadata === undefined) return state.lifecycle
|
||||
@@ -655,10 +650,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 BedrockProviderMetadata.write(state.providerMetadataKey, { signature })
|
||||
const redactedContent = state.reasoningRedactedContent[index]
|
||||
if (redactedContent)
|
||||
return providerMetadata(state.providerMetadataKey, {
|
||||
return BedrockProviderMetadata.write(state.providerMetadataKey, {
|
||||
redactedData: encodeRedactedContent(redactedContent),
|
||||
})
|
||||
})()
|
||||
|
||||
@@ -300,14 +300,15 @@ const lowerUserPart = Effect.fn("Gemini.lowerUserPart")(function* (part: TextPar
|
||||
return { inlineData: { mimeType: media.mime, data: media.base64 } }
|
||||
})
|
||||
|
||||
const providerMetadata = (key: string, metadata: Record<string, unknown>): ProviderMetadata => ({ [key]: metadata })
|
||||
const GeminiProviderMetadata = ProviderShared.providerMetadata(
|
||||
Schema.Struct({
|
||||
thoughtSignature: lenient(Schema.String),
|
||||
promptFeedback: lenient(GeminiPromptFeedback),
|
||||
}),
|
||||
)
|
||||
|
||||
const thoughtSignature = (metadata: ProviderMetadata | undefined, key: string) => {
|
||||
const value = metadata?.[key]
|
||||
return ProviderShared.isRecord(value) && typeof value.thoughtSignature === "string"
|
||||
? value.thoughtSignature
|
||||
: undefined
|
||||
}
|
||||
const thoughtSignature = (metadata: ProviderMetadata | undefined, key: string) =>
|
||||
GeminiProviderMetadata.read(metadata, key)?.thoughtSignature
|
||||
|
||||
const lowerToolCall = (part: ToolCallPart, omitIds: boolean, metadataKey: string) => ({
|
||||
functionCall: { ...(omitIds ? {} : { id: part.id }), name: part.name, args: part.input },
|
||||
@@ -510,7 +511,7 @@ const mapUsage = (usage: GeminiUsage | undefined, metadataKey: string) => {
|
||||
cacheReadInputTokens: cached,
|
||||
reasoningTokens: thoughts,
|
||||
totalTokens: ProviderShared.totalTokens(promptTokens, outputTokens, usage.totalTokenCount ?? undefined),
|
||||
providerMetadata: providerMetadata(metadataKey, usage),
|
||||
providerMetadata: { [metadataKey]: usage },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -560,7 +561,7 @@ const finish = (state: ParserState): ReadonlyArray<LLMEvent> => {
|
||||
state.reasoningId,
|
||||
state.reasoningSignature === undefined
|
||||
? undefined
|
||||
: providerMetadata(state.providerMetadataKey, { thoughtSignature: state.reasoningSignature }),
|
||||
: GeminiProviderMetadata.write(state.providerMetadataKey, { thoughtSignature: state.reasoningSignature }),
|
||||
)
|
||||
if (state.textId !== undefined)
|
||||
lifecycle = Lifecycle.textEnd(
|
||||
@@ -569,7 +570,7 @@ const finish = (state: ParserState): ReadonlyArray<LLMEvent> => {
|
||||
state.textId,
|
||||
state.textSignature === undefined
|
||||
? undefined
|
||||
: providerMetadata(state.providerMetadataKey, { thoughtSignature: state.textSignature }),
|
||||
: GeminiProviderMetadata.write(state.providerMetadataKey, { thoughtSignature: state.textSignature }),
|
||||
)
|
||||
Lifecycle.finish(lifecycle, events, {
|
||||
reason: {
|
||||
@@ -581,7 +582,7 @@ const finish = (state: ParserState): ReadonlyArray<LLMEvent> => {
|
||||
providerMetadata:
|
||||
state.promptFeedback === undefined
|
||||
? undefined
|
||||
: providerMetadata(state.providerMetadataKey, { promptFeedback: state.promptFeedback }),
|
||||
: GeminiProviderMetadata.write(state.providerMetadataKey, { promptFeedback: state.promptFeedback }),
|
||||
})
|
||||
return events
|
||||
}
|
||||
@@ -670,7 +671,7 @@ const step = (state: ParserState, event: GeminiEvent) => {
|
||||
events,
|
||||
textId,
|
||||
textSignature
|
||||
? providerMetadata(state.providerMetadataKey, { thoughtSignature: textSignature })
|
||||
? GeminiProviderMetadata.write(state.providerMetadataKey, { thoughtSignature: textSignature })
|
||||
: undefined,
|
||||
)
|
||||
textId = undefined
|
||||
@@ -685,7 +686,9 @@ const step = (state: ParserState, event: GeminiEvent) => {
|
||||
events,
|
||||
reasoningId,
|
||||
part.text,
|
||||
signature ? providerMetadata(state.providerMetadataKey, { thoughtSignature: signature }) : undefined,
|
||||
signature
|
||||
? GeminiProviderMetadata.write(state.providerMetadataKey, { thoughtSignature: signature })
|
||||
: undefined,
|
||||
)
|
||||
continue
|
||||
}
|
||||
@@ -695,7 +698,7 @@ const step = (state: ParserState, event: GeminiEvent) => {
|
||||
events,
|
||||
reasoningId,
|
||||
reasoningSignature
|
||||
? providerMetadata(state.providerMetadataKey, { thoughtSignature: reasoningSignature })
|
||||
? GeminiProviderMetadata.write(state.providerMetadataKey, { thoughtSignature: reasoningSignature })
|
||||
: undefined,
|
||||
)
|
||||
reasoningId = undefined
|
||||
@@ -710,7 +713,9 @@ const step = (state: ParserState, event: GeminiEvent) => {
|
||||
events,
|
||||
textId,
|
||||
part.text,
|
||||
textSignature ? providerMetadata(state.providerMetadataKey, { thoughtSignature: textSignature }) : undefined,
|
||||
textSignature
|
||||
? GeminiProviderMetadata.write(state.providerMetadataKey, { thoughtSignature: textSignature })
|
||||
: undefined,
|
||||
)
|
||||
textSignature = undefined
|
||||
continue
|
||||
@@ -732,7 +737,7 @@ const step = (state: ParserState, event: GeminiEvent) => {
|
||||
events,
|
||||
reasoningId,
|
||||
reasoningSignature
|
||||
? providerMetadata(state.providerMetadataKey, { thoughtSignature: reasoningSignature })
|
||||
? GeminiProviderMetadata.write(state.providerMetadataKey, { thoughtSignature: reasoningSignature })
|
||||
: undefined,
|
||||
)
|
||||
reasoningId = undefined
|
||||
@@ -743,7 +748,9 @@ const step = (state: ParserState, event: GeminiEvent) => {
|
||||
lifecycle,
|
||||
events,
|
||||
textId,
|
||||
textSignature ? providerMetadata(state.providerMetadataKey, { thoughtSignature: textSignature }) : undefined,
|
||||
textSignature
|
||||
? GeminiProviderMetadata.write(state.providerMetadataKey, { thoughtSignature: textSignature })
|
||||
: undefined,
|
||||
)
|
||||
textId = undefined
|
||||
textSignature = undefined
|
||||
@@ -755,7 +762,7 @@ const step = (state: ParserState, event: GeminiEvent) => {
|
||||
name: part.functionCall.name,
|
||||
input,
|
||||
providerMetadata: part.thoughtSignature
|
||||
? providerMetadata(state.providerMetadataKey, { thoughtSignature: part.thoughtSignature })
|
||||
? GeminiProviderMetadata.write(state.providerMetadataKey, { thoughtSignature: part.thoughtSignature })
|
||||
: undefined,
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -11,14 +11,13 @@ import {
|
||||
type JsonSchema,
|
||||
type LLMRequest,
|
||||
type MediaPart,
|
||||
type ProviderMetadata,
|
||||
type ReasoningPart,
|
||||
type TextPart,
|
||||
type ToolCallPart,
|
||||
type ToolDefinition,
|
||||
type ToolResultPart,
|
||||
} from "../schema/index.js"
|
||||
import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js"
|
||||
import { JsonObject, lenient, optionalArray, optionalNull, ProviderShared } from "./shared.js"
|
||||
import { classifyProviderFailure } from "../provider-error.js"
|
||||
import { effortUpdate } from "../effort-updates.js"
|
||||
import { OpenResponsesOptions } from "./utils/open-responses-options.js"
|
||||
@@ -521,17 +520,27 @@ export const lowerToolChoice = (protocolName: string, toolChoice: NonNullable<LL
|
||||
tool: (toolName) => ({ type: "function" as const, name: toolName }),
|
||||
})
|
||||
|
||||
export const providerMetadata = ProviderShared.providerMetadata(
|
||||
Schema.Struct({
|
||||
itemId: lenient(Schema.String),
|
||||
phase: lenient(MessagePhase),
|
||||
reasoningEncryptedContent: lenient(Schema.NullOr(Schema.String)),
|
||||
responseId: lenient(Schema.String),
|
||||
serviceTier: lenient(Schema.NullOr(Schema.String)),
|
||||
}),
|
||||
)
|
||||
|
||||
// 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]
|
||||
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 itemID = (metadata: ReturnType<typeof providerMetadata.read>) => {
|
||||
const itemId = metadata?.itemId
|
||||
if (itemId === undefined) return undefined
|
||||
const separator = itemId.indexOf("_")
|
||||
return separator > 0 && separator < itemId.length - 1 ? itemId : undefined
|
||||
}
|
||||
|
||||
const lowerToolCall = (part: ToolCallPart, providerMetadataKey: string): OpenResponsesInputItem => {
|
||||
const id = itemID(part.providerMetadata, providerMetadataKey)
|
||||
const id = itemID(providerMetadata.read(part.providerMetadata, providerMetadataKey))
|
||||
return {
|
||||
type: "function_call",
|
||||
...(id === undefined ? {} : { id }),
|
||||
@@ -543,18 +552,14 @@ const lowerToolCall = (part: ToolCallPart, providerMetadataKey: string): OpenRes
|
||||
}
|
||||
|
||||
const lowerReasoning = (part: ReasoningPart, providerMetadataKey: string): OpenResponsesReasoningInput | undefined => {
|
||||
const metadata = part.providerMetadata?.[providerMetadataKey]
|
||||
if (!ProviderShared.isRecord(metadata)) return undefined
|
||||
const id = itemID(part.providerMetadata, providerMetadataKey)
|
||||
const encryptedContent =
|
||||
typeof metadata.reasoningEncryptedContent === "string" || metadata.reasoningEncryptedContent === null
|
||||
? metadata.reasoningEncryptedContent
|
||||
: undefined
|
||||
const metadata = providerMetadata.read(part.providerMetadata, providerMetadataKey)
|
||||
if (!metadata) return undefined
|
||||
const id = itemID(metadata)
|
||||
return {
|
||||
type: "reasoning",
|
||||
...(id === undefined ? {} : { id }),
|
||||
summary: part.text.length > 0 ? [{ type: "summary_text", text: part.text }] : [],
|
||||
encrypted_content: encryptedContent,
|
||||
encrypted_content: metadata.reasoningEncryptedContent,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -696,10 +701,9 @@ 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 partPhase = messagePhase(partMetadata?.phase)
|
||||
const phase = partPhase === undefined ? metadata?.phase : partPhase
|
||||
const partMetadata = providerMetadata.read(part.providerMetadata, providerMetadataKey)
|
||||
const id = itemID(partMetadata) ?? metadata?.itemId
|
||||
const phase = partMetadata?.phase === undefined ? metadata?.phase : partMetadata.phase
|
||||
const group = groups.at(-1)
|
||||
if (group && group.id === id && group.phase === phase) group.parts.push(part)
|
||||
else groups.push({ id, phase, parts: [part] })
|
||||
@@ -755,7 +759,7 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (
|
||||
}
|
||||
if (part.type === "tool-result" && part.providerExecuted === true) {
|
||||
flushText()
|
||||
const id = itemID(part.providerMetadata, providerMetadataKey)
|
||||
const id = itemID(providerMetadata.read(part.providerMetadata, providerMetadataKey))
|
||||
const hosted =
|
||||
part.result.type !== "json"
|
||||
? undefined
|
||||
@@ -937,10 +941,6 @@ const mapFinishReason = (event: Event, hasFunctionCall: boolean): FinishReason =
|
||||
|
||||
export const metadataKey = (model: LLMRequest["model"]) => model.route.providerMetadataKey ?? "openresponses"
|
||||
|
||||
export const providerMetadata = (state: ParserState, metadata: Record<string, unknown>): ProviderMetadata => ({
|
||||
[state.providerMetadataKey]: metadata,
|
||||
})
|
||||
|
||||
export type StepResult = readonly [ParserState, ReadonlyArray<LLMEvent>]
|
||||
|
||||
const NO_EVENTS: StepResult["1"] = []
|
||||
@@ -955,7 +955,10 @@ const onOutputTextDelta = (state: ParserState, event: Event, id: string): StepRe
|
||||
if (!event.delta || state.message?.id !== id) return [state, NO_EVENTS]
|
||||
const events: LLMEvent[] = []
|
||||
const phase = state.message.phase
|
||||
const metadata = providerMetadata(state, { itemId: id, ...(phase === undefined ? {} : { phase }) })
|
||||
const metadata = providerMetadata.write(state.providerMetadataKey, {
|
||||
itemId: id,
|
||||
...(phase === undefined ? {} : { phase }),
|
||||
})
|
||||
const lifecycle = Lifecycle.textStart(state.lifecycle, events, id, metadata)
|
||||
return [{ ...state, lifecycle: Lifecycle.textDelta(lifecycle, events, id, event.delta) }, events]
|
||||
}
|
||||
@@ -1025,7 +1028,12 @@ const startReasoningSummaryPart = (state: ParserState, itemID: string, index: nu
|
||||
.filter((entry) => entry[1] !== "concluded")
|
||||
.reduce(
|
||||
(lifecycle, entry) =>
|
||||
Lifecycle.reasoningEnd(lifecycle, events, `${itemID}:${entry[0]}`, providerMetadata(state, { itemId: itemID })),
|
||||
Lifecycle.reasoningEnd(
|
||||
lifecycle,
|
||||
events,
|
||||
`${itemID}:${entry[0]}`,
|
||||
providerMetadata.write(state.providerMetadataKey, { itemId: itemID }),
|
||||
),
|
||||
state.lifecycle,
|
||||
)
|
||||
return [
|
||||
@@ -1035,7 +1043,10 @@ const startReasoningSummaryPart = (state: ParserState, itemID: string, index: nu
|
||||
lifecycle,
|
||||
events,
|
||||
`${itemID}:${index}`,
|
||||
providerMetadata(state, { itemId: itemID, reasoningEncryptedContent: item.encryptedContent ?? null }),
|
||||
providerMetadata.write(state.providerMetadataKey, {
|
||||
itemId: itemID,
|
||||
reasoningEncryptedContent: item.encryptedContent ?? null,
|
||||
}),
|
||||
),
|
||||
reasoningItems: {
|
||||
...state.reasoningItems,
|
||||
@@ -1090,7 +1101,10 @@ export const onReasoningDone = (state: ParserState, event: Event, itemID: string
|
||||
}
|
||||
|
||||
const reasoningMetadata = (state: ParserState, item: OutputItem) =>
|
||||
providerMetadata(state, { itemId: item.id, reasoningEncryptedContent: item.encrypted_content ?? null })
|
||||
providerMetadata.write(state.providerMetadataKey, {
|
||||
itemId: item.id,
|
||||
reasoningEncryptedContent: item.encrypted_content ?? null,
|
||||
})
|
||||
|
||||
// Responses APIs normally stream reasoning items in this order:
|
||||
// `output_item.added` (reasoning) →
|
||||
@@ -1117,7 +1131,10 @@ const onOutputItemAdded = (state: ParserState, event: NormalizedEvent): StepResu
|
||||
lifecycle,
|
||||
events,
|
||||
id,
|
||||
providerMetadata(state, { itemId: id, ...(openPhase === undefined ? {} : { phase: openPhase }) }),
|
||||
providerMetadata.write(state.providerMetadataKey, {
|
||||
itemId: id,
|
||||
...(openPhase === undefined ? {} : { phase: openPhase }),
|
||||
}),
|
||||
)
|
||||
}, state.lifecycle)
|
||||
return [
|
||||
@@ -1153,7 +1170,7 @@ const onOutputItemAdded = (state: ParserState, event: NormalizedEvent): StepResu
|
||||
}
|
||||
if (item.type !== "function_call" || !item.call_id) return [state, NO_EVENTS]
|
||||
if (state.tools[item.id] !== undefined) return [state, NO_EVENTS]
|
||||
const metadata = providerMetadata(state, { itemId: item.id })
|
||||
const metadata = providerMetadata.write(state.providerMetadataKey, { itemId: item.id })
|
||||
const events: LLMEvent[] = []
|
||||
const lifecycle = Lifecycle.stepStart(state.lifecycle, events)
|
||||
return [
|
||||
@@ -1276,7 +1293,10 @@ const onOutputItemDone = Effect.fn("OpenResponses.onOutputItemDone")(function* (
|
||||
content.push(decoded.type === "output_text" ? decoded.text : decoded.refusal)
|
||||
}
|
||||
const text = content.length > 0 ? content.join("") : undefined
|
||||
const metadata = providerMetadata(state, { itemId: item.id, ...(phase === undefined ? {} : { phase }) })
|
||||
const metadata = providerMetadata.write(state.providerMetadataKey, {
|
||||
itemId: item.id,
|
||||
...(phase === undefined ? {} : { phase }),
|
||||
})
|
||||
const events: LLMEvent[] = []
|
||||
const lifecycle = text ? Lifecycle.textStart(state.lifecycle, events, item.id, metadata) : state.lifecycle
|
||||
return [
|
||||
@@ -1291,7 +1311,7 @@ const onOutputItemDone = Effect.fn("OpenResponses.onOutputItemDone")(function* (
|
||||
|
||||
if (item.type === "function_call") {
|
||||
if (!item.call_id || !item.name) return [state, NO_EVENTS] satisfies StepResult
|
||||
const metadata = providerMetadata(state, { itemId: item.id })
|
||||
const metadata = providerMetadata.write(state.providerMetadataKey, { itemId: item.id })
|
||||
const registered = state.tools[item.id] !== undefined
|
||||
const tools = registered
|
||||
? state.tools
|
||||
@@ -1413,7 +1433,7 @@ const onResponseFinish = Effect.fn("OpenResponses.onResponseFinish")(function* (
|
||||
usage: mapUsage(event.response?.usage, current.providerMetadataKey),
|
||||
providerMetadata:
|
||||
event.response?.id || event.response?.service_tier
|
||||
? providerMetadata(current, {
|
||||
? providerMetadata.write(current.providerMetadataKey, {
|
||||
responseId: event.response.id,
|
||||
serviceTier: event.response.service_tier,
|
||||
})
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
type ToolDefinition,
|
||||
} from "../schema/index.js"
|
||||
import { classifyProviderFailure } from "../provider-error.js"
|
||||
import { isRecord, JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js"
|
||||
import { isRecord, JsonObject, lenient, optionalArray, optionalNull, ProviderShared } from "./shared.js"
|
||||
import { OpenAIOptions } from "./utils/openai-options.js"
|
||||
import { Lifecycle } from "./utils/lifecycle.js"
|
||||
import { ToolSchemaProjection } from "./utils/tool-schema.js"
|
||||
@@ -333,18 +333,20 @@ 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
|
||||
return typeof field === "string" ? field : undefined
|
||||
}
|
||||
const OpenAIChatProviderMetadata = ProviderShared.providerMetadata(
|
||||
Schema.Struct({
|
||||
reasoningField: lenient(Schema.String),
|
||||
// OpenRouter-defined passthrough; its element shape is not modelled.
|
||||
reasoningDetails: lenient(Schema.Array(Schema.Unknown)),
|
||||
}),
|
||||
)
|
||||
|
||||
const reasoningDetails = (parts: ReadonlyArray<ReasoningPart>, native: unknown, providerMetadataKey: string) => {
|
||||
const observed = parts.flatMap((part) => {
|
||||
const details = part.providerMetadata?.[providerMetadataKey]?.reasoningDetails
|
||||
return Array.isArray(details) ? details : []
|
||||
})
|
||||
if (parts.some((part) => Array.isArray(part.providerMetadata?.[providerMetadataKey]?.reasoningDetails)))
|
||||
return observed
|
||||
const reasoningDetails = (
|
||||
observed: ReadonlyArray<ReturnType<typeof OpenAIChatProviderMetadata.read>>,
|
||||
native: unknown,
|
||||
) => {
|
||||
if (observed.some((metadata) => metadata?.reasoningDetails !== undefined))
|
||||
return observed.flatMap((metadata) => metadata?.reasoningDetails ?? [])
|
||||
if (isRecord(native) && Array.isArray(native.reasoning_details)) return native.reasoning_details
|
||||
}
|
||||
|
||||
@@ -398,14 +400,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 observedField = reasoning
|
||||
.map((part) => reasoningField(part, options.providerMetadataKey))
|
||||
.find((value) => value !== undefined)
|
||||
const nativeReasoning = openAICompatibleReasoningContent(message.native?.openaiCompatible)
|
||||
const fullyStructured = reasoning.every((part) =>
|
||||
Array.isArray(part.providerMetadata?.[options.providerMetadataKey]?.reasoningDetails),
|
||||
const observed = reasoning.map((part) =>
|
||||
OpenAIChatProviderMetadata.read(part.providerMetadata, options.providerMetadataKey),
|
||||
)
|
||||
const details = reasoningDetails(observed, message.native?.openaiCompatible)
|
||||
const observedField = observed.map((metadata) => metadata?.reasoningField).find((value) => value !== undefined)
|
||||
const nativeReasoning = openAICompatibleReasoningContent(message.native?.openaiCompatible)
|
||||
const fullyStructured = observed.every((metadata) => metadata?.reasoningDetails !== undefined)
|
||||
const field = (() => {
|
||||
if (configuredField !== undefined && (requireReasoning || reasoning.length > 0 || nativeReasoning !== undefined))
|
||||
return configuredField
|
||||
@@ -937,12 +938,11 @@ const reasoningMetadata = (
|
||||
providerMetadataKey: string,
|
||||
field: ParserState["reasoningField"],
|
||||
details?: ReadonlyArray<unknown>,
|
||||
) => ({
|
||||
[providerMetadataKey]: {
|
||||
) =>
|
||||
OpenAIChatProviderMetadata.write(providerMetadataKey, {
|
||||
...(field ? { reasoningField: field } : {}),
|
||||
...(details ? { reasoningDetails: details } : {}),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
const step = (state: ParserState, event: OpenAIChatEvent) =>
|
||||
Effect.gen(function* () {
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
type ContentPart,
|
||||
type MediaPart,
|
||||
type ProviderID,
|
||||
type ProviderMetadata,
|
||||
type TextPart,
|
||||
type ToolEntry,
|
||||
type ToolResultPart,
|
||||
@@ -34,6 +35,17 @@ export const lenient = <const S extends Schema.Top>(schema: S) =>
|
||||
Schema.optionalKey(
|
||||
Schema.UndefinedOr(schema).pipe(Schema.catchDecoding(() => Effect.succeed(Option.some(undefined)))),
|
||||
)
|
||||
/**
|
||||
* Typed slice of `ProviderMetadata` a protocol publishes under the route's `providerMetadataKey`.
|
||||
* Declare fields with `lenient` so `read` drops a malformed field without discarding its siblings.
|
||||
*/
|
||||
export const providerMetadata = <const S extends Schema.ConstraintDecoder<Record<string, unknown>>>(schema: S) => {
|
||||
const decode = Schema.decodeUnknownOption(schema)
|
||||
return {
|
||||
write: (key: string, value: S["Type"]) => ({ [key]: value }),
|
||||
read: (metadata: ProviderMetadata | undefined, key: string) => Option.getOrUndefined(decode(metadata?.[key])),
|
||||
}
|
||||
}
|
||||
/** Provider-defined string enum: known values for autocomplete, any string accepted at runtime. */
|
||||
export const knownString = <Known extends string>() =>
|
||||
Schema.declare<Known | (string & {})>((value): value is Known | (string & {}) => typeof value === "string", {
|
||||
|
||||
@@ -144,7 +144,10 @@ function toMessage(item: (typeof Response.Type.output)[number], model: LLMReques
|
||||
summary.map((part) => ({
|
||||
type: "reasoning" as const,
|
||||
text: part.text,
|
||||
providerMetadata: { [key]: { itemId: item.id, reasoningEncryptedContent: item.encrypted_content } },
|
||||
providerMetadata: OpenResponses.providerMetadata.write(key, {
|
||||
itemId: item.id,
|
||||
reasoningEncryptedContent: item.encrypted_content,
|
||||
}),
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ export const onDone: (
|
||||
function* (state, item, tools) {
|
||||
const tool = tools[item.type]
|
||||
if (!tool) return [state, []] satisfies OpenResponses.StepResult
|
||||
const providerMetadata = OpenResponses.providerMetadata(state, { itemId: item.id })
|
||||
const providerMetadata = OpenResponses.providerMetadata.write(state.providerMetadataKey, { itemId: item.id })
|
||||
const events: LLMEvent[] = []
|
||||
const lifecycle = Lifecycle.stepStart(state.lifecycle, events)
|
||||
events.push(
|
||||
|
||||
@@ -916,6 +916,42 @@ describe("Anthropic Messages route", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("drops malformed provider metadata fields without discarding their siblings", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
LLM.request({
|
||||
model,
|
||||
messages: [
|
||||
Message.assistant([
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "",
|
||||
providerMetadata: { anthropic: { signature: 123, redactedData: "opaque_1" } },
|
||||
},
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "visible",
|
||||
providerMetadata: { anthropic: { signature: "sig_1", redactedData: 7 } },
|
||||
},
|
||||
]),
|
||||
],
|
||||
}),
|
||||
)
|
||||
|
||||
expect(prepared.body).toMatchObject({
|
||||
messages: [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "redacted_thinking", data: "opaque_1" },
|
||||
{ type: "thinking", thinking: "visible", signature: "sig_1" },
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("round-trips compatible provider metadata in its own namespace", () =>
|
||||
Effect.gen(function* () {
|
||||
const compatible = Route.make({
|
||||
|
||||
Reference in New Issue
Block a user