mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 10:30:01 +08:00
refactor(ai): share OpenRouter Responses protocol
This commit is contained in:
@@ -1210,8 +1210,10 @@ LLM.request({
|
||||
Normalized cache usage is read back into `response.usage.cacheReadInputTokens` and `cacheWriteInputTokens` across every provider.
|
||||
|
||||
OpenRouter selects its wire API by model family: `openai/*`, `x-ai/*`, and `meta/*` use Responses; `anthropic/*` uses
|
||||
Anthropic Messages; other families use Chat Completions. Leading-`~` model aliases follow the same routing. Explicit
|
||||
`openrouter.chat(id)`, `.responses(id)`, and `.messages(id)` selectors are available for API-specific integrations.
|
||||
Anthropic Messages; other families use Chat Completions. All three Responses families share the provider-neutral
|
||||
Responses serializer and stream parser; there are no per-vendor OpenRouter protocols. Leading-`~` model aliases follow
|
||||
the same routing. Explicit `openrouter.chat(id)`, `.responses(id)`, and `.messages(id)` selectors are available for
|
||||
API-specific integrations.
|
||||
Qwen retains Chat's explicit system/conversation cache markers, while Claude uses native Messages cache blocks.
|
||||
OpenRouter Responses is stateless (`store: false`); every request includes full history, not `previous_response_id`.
|
||||
Meta currently accepts only automatic tool selection on both its Chat and Responses upstreams; forced/disabled tool
|
||||
|
||||
@@ -44,7 +44,7 @@ const ImageItem = Schema.Struct({
|
||||
error: Schema.optional(Schema.Unknown),
|
||||
})
|
||||
|
||||
export const Body = Schema.Struct({
|
||||
const Body = Schema.Struct({
|
||||
...OpenResponses.coreFields,
|
||||
input: Schema.Array(Schema.Union([OpenResponses.InputItem, ImageItem])),
|
||||
tools: optionalArray(Schema.Union([OpenResponses.Tool, NativeTool])),
|
||||
|
||||
@@ -163,7 +163,7 @@ export const CompactionItem = Schema.Struct({
|
||||
encrypted_content: Schema.String,
|
||||
})
|
||||
|
||||
// Kept out of the baseline `InputItem` union: only the OpenAI extension accepts it.
|
||||
// Kept out of the baseline `InputItem` union; extensions such as OpenAI and OpenRouter opt in.
|
||||
export const ConfigurationUpdate = Schema.Struct({
|
||||
type: Schema.Literal("configuration_update"),
|
||||
reasoning: Schema.Struct({ effort: OpenResponsesOptions.ReasoningEffort }),
|
||||
|
||||
@@ -124,7 +124,7 @@ const OpenAIResponsesCoreFields = {
|
||||
),
|
||||
}
|
||||
|
||||
export const OpenAIResponsesBody = Schema.Struct({
|
||||
const OpenAIResponsesBody = Schema.Struct({
|
||||
...OpenAIResponsesCoreFields,
|
||||
stream: Schema.Literal(true),
|
||||
})
|
||||
|
||||
@@ -2,7 +2,9 @@ export * as OpenRouterWire from "./openrouter.js"
|
||||
|
||||
import { Option, Schema } from "effect"
|
||||
import { LLMRequest, Message, type ContentPart, type ReasoningPart } from "../../schema/index.js"
|
||||
import { resolveEffortUpdates } from "../../effort-updates.js"
|
||||
import type { OpenResponses } from "../open-responses.js"
|
||||
import { OpenResponsesOptions } from "./open-responses-options.js"
|
||||
import { isRecord, ProviderShared } from "../shared.js"
|
||||
|
||||
const ReplayDetail = Schema.Struct({
|
||||
@@ -14,19 +16,33 @@ const ReplayDetail = Schema.Struct({
|
||||
})
|
||||
const decodeReplayDetail = Schema.decodeUnknownOption(ReplayDetail)
|
||||
|
||||
// OpenRouter's generic Responses API supports chronological effort updates only on eligible models.
|
||||
export function supportsEffortUpdates(request: LLMRequest) {
|
||||
if (request.providerOptions?.truncation === "auto" || request.http?.body?.truncation === "auto") return false
|
||||
if (Schema.is(Schema.Struct({ mode: Schema.Literal("pro") }))(request.providerOptions?.reasoning)) return false
|
||||
if (Schema.is(Schema.Struct({ mode: Schema.Literal("pro") }))(request.http?.body?.reasoning)) return false
|
||||
return (
|
||||
request.model.compatibility?.supportsEffortUpdates ?? /^~?openai\/gpt-6-(?:astra|sol|luna)$/i.test(request.model.id)
|
||||
)
|
||||
}
|
||||
|
||||
export function nativeRequest(request: LLMRequest, format: "responses" | "messages") {
|
||||
const options = request.providerOptions ?? {}
|
||||
const reasoning = isRecord(options.reasoning)
|
||||
? fitReasoning(options.reasoning, request.generation?.maxTokens)
|
||||
: undefined
|
||||
const disabled = reasoning?.enabled === false || reasoning?.effort === "none"
|
||||
const updates = resolveEffortUpdates(
|
||||
request,
|
||||
OpenResponsesOptions.resolve(request).reasoningEffort ??
|
||||
(typeof reasoning?.effort === "string" ? reasoning.effort : undefined),
|
||||
)
|
||||
return LLMRequest.update(request, {
|
||||
providerOptions:
|
||||
format === "responses"
|
||||
? {
|
||||
...options,
|
||||
reasoningEffort:
|
||||
options.reasoningEffort ?? (typeof reasoning?.effort === "string" ? reasoning.effort : undefined),
|
||||
reasoningEffort: updates.effort,
|
||||
}
|
||||
: {
|
||||
...options,
|
||||
@@ -42,7 +58,7 @@ export function nativeRequest(request: LLMRequest, format: "responses" | "messag
|
||||
return { type: "adaptive", ...(reasoning.exclude === true ? { display: "omitted" } : {}) }
|
||||
})(),
|
||||
},
|
||||
messages: request.messages.map((message) => {
|
||||
messages: (format === "responses" ? updates.request : request).messages.map((message) => {
|
||||
if (
|
||||
!message.content.some(
|
||||
(part) => part.type === "reasoning" && part.providerMetadata?.openrouter?.reasoningDetails !== undefined,
|
||||
|
||||
@@ -31,7 +31,7 @@ const XAIResponsesHostedToolItem = Schema.Union([
|
||||
),
|
||||
])
|
||||
|
||||
export const XAIResponsesBody = Schema.Struct({
|
||||
const XAIResponsesBody = Schema.Struct({
|
||||
...OpenResponses.coreFields,
|
||||
input: Schema.Array(Schema.Union([OpenResponses.InputItem, XAIResponsesHostedToolItem])),
|
||||
stream: Schema.Literal(true),
|
||||
|
||||
@@ -7,11 +7,10 @@ import { HttpOptions, ProviderID, type CacheHint, type ModelID, type OpenString
|
||||
import type { ProviderPackage } from "../provider-package.js"
|
||||
import { SystemOne } from "../experimental/system-one.js"
|
||||
import { OpenAIChat } from "../protocols/openai-chat.js"
|
||||
import { OpenAIResponses } from "../protocols/openai-responses.js"
|
||||
import { XAIResponses } from "../protocols/xai-responses.js"
|
||||
import { MetaResponses } from "../protocols/meta-responses.js"
|
||||
import { OpenResponses } from "../protocols/open-responses.js"
|
||||
import { AnthropicMessages } from "../protocols/anthropic-messages.js"
|
||||
import { Framing } from "../route/framing.js"
|
||||
import { ProviderShared } from "../protocols/shared.js"
|
||||
import { newBreakpoints, ttlBucket } from "../protocols/utils/cache.js"
|
||||
import { OpenRouterWire } from "../protocols/utils/openrouter.js"
|
||||
|
||||
@@ -110,20 +109,14 @@ const ResponsesReasoning = Schema.StructWithRest(
|
||||
}),
|
||||
[Schema.Record(Schema.String, Schema.Unknown)],
|
||||
)
|
||||
const responsesFields = {
|
||||
store: Schema.Literal(false),
|
||||
reasoning: Schema.optional(ResponsesReasoning),
|
||||
}
|
||||
const OpenRouterResponsesBody = Schema.StructWithRest(
|
||||
Schema.Struct({ ...OpenAIResponses.OpenAIResponsesBody.fields, ...responsesFields }),
|
||||
[Schema.Record(Schema.String, Schema.Unknown)],
|
||||
)
|
||||
const OpenRouterXAIResponsesBody = Schema.StructWithRest(
|
||||
Schema.Struct({ ...XAIResponses.XAIResponsesBody.fields, ...responsesFields }),
|
||||
[Schema.Record(Schema.String, Schema.Unknown)],
|
||||
)
|
||||
const OpenRouterMetaResponsesBody = Schema.StructWithRest(
|
||||
Schema.Struct({ ...MetaResponses.Body.fields, ...responsesFields }),
|
||||
Schema.Struct({
|
||||
...OpenResponses.coreFields,
|
||||
input: Schema.Array(Schema.Union([OpenResponses.InputItem, OpenResponses.ConfigurationUpdate])),
|
||||
stream: Schema.Literal(true),
|
||||
store: Schema.Literal(false),
|
||||
reasoning: Schema.optional(ResponsesReasoning),
|
||||
}),
|
||||
[Schema.Record(Schema.String, Schema.Unknown)],
|
||||
)
|
||||
const OpenRouterMessagesBody = Schema.StructWithRest(AnthropicMessages.AnthropicMessagesBody, [
|
||||
@@ -168,36 +161,19 @@ export const protocol = Protocol.make({
|
||||
})
|
||||
|
||||
const responsesProtocol = Protocol.make({
|
||||
...OpenAIResponses.protocol,
|
||||
...OpenResponses.protocol,
|
||||
id: "openrouter-responses",
|
||||
supportsEffortUpdates: OpenRouterWire.supportsEffortUpdates,
|
||||
body: {
|
||||
schema: OpenRouterResponsesBody,
|
||||
from: (request) =>
|
||||
OpenAIResponses.protocol.body
|
||||
.from(OpenRouterWire.nativeRequest(request, "responses"))
|
||||
.pipe(Effect.map((body) => OpenRouterWire.responsesOptions(request, body))),
|
||||
},
|
||||
})
|
||||
const xaiProtocol = Protocol.make({
|
||||
...XAIResponses.protocol,
|
||||
id: "openrouter-xai-responses",
|
||||
body: {
|
||||
schema: OpenRouterXAIResponsesBody,
|
||||
from: (request) =>
|
||||
XAIResponses.protocol.body
|
||||
.from(OpenRouterWire.nativeRequest(request, "responses"))
|
||||
.pipe(Effect.map((body) => OpenRouterWire.responsesOptions(request, body))),
|
||||
},
|
||||
})
|
||||
const metaProtocol = Protocol.make({
|
||||
...MetaResponses.protocol,
|
||||
id: "openrouter-meta-responses",
|
||||
body: {
|
||||
schema: OpenRouterMetaResponsesBody,
|
||||
from: (request) =>
|
||||
MetaResponses.protocol.body
|
||||
.from(OpenRouterWire.nativeRequest(request, "responses"))
|
||||
.pipe(Effect.map((body) => OpenRouterWire.responsesOptions(request, body))),
|
||||
OpenResponses.fromRequestWithAdapter(OpenRouterWire.nativeRequest(request, "responses"), {
|
||||
id: "openrouter-responses",
|
||||
name: "OpenRouter Responses",
|
||||
}).pipe(
|
||||
Effect.map((body) => OpenRouterWire.responsesOptions(request, body)),
|
||||
Effect.flatMap(ProviderShared.validateWith(Schema.decodeUnknownEffect(OpenRouterResponsesBody))),
|
||||
),
|
||||
},
|
||||
})
|
||||
const messagesProtocol = Protocol.make({
|
||||
@@ -249,24 +225,6 @@ const responsesRoute = Route.make({
|
||||
framing: Framing.sse,
|
||||
defaults: { providerOptions: { store: false, include: ["reasoning.encrypted_content"] } },
|
||||
})
|
||||
const xaiRoute = Route.make({
|
||||
id: "openrouter-xai-responses",
|
||||
provider: id,
|
||||
providerMetadataKey: "openrouter",
|
||||
protocol: xaiProtocol,
|
||||
endpoint: Endpoint.path("/responses", { baseURL }),
|
||||
framing: Framing.sse,
|
||||
defaults: { providerOptions: { store: false, include: ["reasoning.encrypted_content"] } },
|
||||
})
|
||||
const metaRoute = Route.make({
|
||||
id: "openrouter-meta-responses",
|
||||
provider: id,
|
||||
providerMetadataKey: "openrouter",
|
||||
protocol: metaProtocol,
|
||||
endpoint: Endpoint.path("/responses", { baseURL }),
|
||||
framing: Framing.sse,
|
||||
defaults: { providerOptions: { store: false, include: ["reasoning.encrypted_content"] } },
|
||||
})
|
||||
const messagesRoute = Route.make({
|
||||
id: "openrouter-messages",
|
||||
provider: id,
|
||||
@@ -276,7 +234,7 @@ const messagesRoute = Route.make({
|
||||
transport: AnthropicMessages.transport<OpenRouterMessagesBody>(),
|
||||
})
|
||||
|
||||
export const routes = [route, responsesRoute, xaiRoute, metaRoute, messagesRoute]
|
||||
export const routes = [route, responsesRoute, messagesRoute]
|
||||
|
||||
const routeOptions = (input: LanguageModelOptions) => {
|
||||
const { apiKey: _, auth: _auth, baseURL: endpoint, ...rest } = input
|
||||
@@ -290,18 +248,11 @@ const routeOptions = (input: LanguageModelOptions) => {
|
||||
export const configure = (input: LanguageModelOptions = {}) => {
|
||||
const options = routeOptions(input)
|
||||
const chatRoute = route.with(options)
|
||||
const openaiResponses = responsesRoute.with(options)
|
||||
const xaiResponses = xaiRoute.with(options)
|
||||
const metaResponses = metaRoute.with(options)
|
||||
const openResponses = responsesRoute.with(options)
|
||||
const anthropicMessages = messagesRoute.with(options)
|
||||
const chat = (modelID: string | ModelID) =>
|
||||
chatRoute.model<OpenRouterProviderOptionsInput>({ id: modelID, compatibility: { supportsPromptCacheKey: true } })
|
||||
const responses = (modelID: string | ModelID) => {
|
||||
const model = String(modelID).replace(/^~/, "")
|
||||
return (
|
||||
model.startsWith("x-ai/") ? xaiResponses : model.startsWith("meta/") ? metaResponses : openaiResponses
|
||||
).model<OpenRouterProviderOptionsInput>({ id: modelID })
|
||||
}
|
||||
const responses = (modelID: string | ModelID) => openResponses.model<OpenRouterProviderOptionsInput>({ id: modelID })
|
||||
const messages = (modelID: string | ModelID) =>
|
||||
anthropicMessages.model<OpenRouterProviderOptionsInput>({ id: modelID })
|
||||
const evaluation = (modelID: string | ModelID) =>
|
||||
|
||||
+2
-2
@@ -12,7 +12,7 @@
|
||||
"golden"
|
||||
],
|
||||
"name": "openrouter-responses/openrouter-gpt-4o-mini-text",
|
||||
"recordedAt": "2026-09-30T03:26:07.521Z"
|
||||
"recordedAt": "2026-09-30T04:48:47.207Z"
|
||||
},
|
||||
"interactions": [
|
||||
{
|
||||
@@ -30,7 +30,7 @@
|
||||
"headers": {
|
||||
"content-type": "text/event-stream"
|
||||
},
|
||||
"body": "data: {\"type\":\"response.created\",\"response\":{\"id\":\"gen-1790738766-AxuIaR7Dk36zBth5HcUa\",\"object\":\"response\",\"created_at\":1790738766,\"model\":\"openai/gpt-4o-mini\",\"status\":\"in_progress\",\"completed_at\":null,\"output\":[],\"error\":null,\"incomplete_details\":null,\"tools\":[],\"tool_choice\":\"auto\",\"parallel_tool_calls\":true,\"max_output_tokens\":40,\"temperature\":0,\"top_p\":1,\"presence_penalty\":0,\"frequency_penalty\":0,\"top_logprobs\":0,\"max_tool_calls\":null,\"metadata\":{},\"background\":false,\"previous_response_id\":null,\"service_tier\":\"auto\",\"truncation\":\"disabled\",\"store\":false,\"instructions\":\"You are concise.\",\"text\":{\"format\":{\"type\":\"text\"}},\"reasoning\":null,\"safety_identifier\":null,\"prompt_cache_key\":null,\"prompt_cache_options\":null,\"usage\":null},\"sequence_number\":0}\n\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"gen-1790738766-AxuIaR7Dk36zBth5HcUa\",\"object\":\"response\",\"created_at\":1790738766,\"model\":\"openai/gpt-4o-mini\",\"status\":\"in_progress\",\"completed_at\":null,\"output\":[],\"error\":null,\"incomplete_details\":null,\"tools\":[],\"tool_choice\":\"auto\",\"parallel_tool_calls\":true,\"max_output_tokens\":40,\"temperature\":0,\"top_p\":1,\"presence_penalty\":0,\"frequency_penalty\":0,\"top_logprobs\":0,\"max_tool_calls\":null,\"metadata\":{},\"background\":false,\"previous_response_id\":null,\"service_tier\":\"auto\",\"truncation\":\"disabled\",\"store\":false,\"instructions\":\"You are concise.\",\"text\":{\"format\":{\"type\":\"text\"}},\"reasoning\":null,\"safety_identifier\":null,\"prompt_cache_key\":null,\"prompt_cache_options\":null,\"usage\":null},\"sequence_number\":1}\n\ndata: {\"type\":\"response.output_item.added\",\"output_index\":0,\"item\":{\"id\":\"msg_tmp_qpwvyuytpog\",\"type\":\"message\",\"status\":\"in_progress\",\"role\":\"assistant\",\"content\":[]},\"sequence_number\":2}\n\ndata: {\"type\":\"response.content_part.added\",\"output_index\":0,\"item_id\":\"msg_tmp_qpwvyuytpog\",\"content_index\":0,\"part\":{\"type\":\"output_text\",\"text\":\"\",\"annotations\":[],\"logprobs\":[]},\"sequence_number\":3}\n\ndata: {\"type\":\"response.output_text.delta\",\"output_index\":0,\"item_id\":\"msg_tmp_qpwvyuytpog\",\"content_index\":0,\"delta\":\"Hello\",\"logprobs\":[],\"sequence_number\":4}\n\ndata: {\"type\":\"response.output_text.delta\",\"output_index\":0,\"item_id\":\"msg_tmp_qpwvyuytpog\",\"content_index\":0,\"delta\":\"!\",\"logprobs\":[],\"sequence_number\":5}\n\ndata: {\"type\":\"response.output_text.done\",\"output_index\":0,\"item_id\":\"msg_tmp_qpwvyuytpog\",\"content_index\":0,\"text\":\"Hello!\",\"logprobs\":[],\"sequence_number\":6}\n\ndata: {\"type\":\"response.content_part.done\",\"output_index\":0,\"item_id\":\"msg_tmp_qpwvyuytpog\",\"content_index\":0,\"part\":{\"type\":\"output_text\",\"text\":\"Hello!\",\"annotations\":[],\"logprobs\":[]},\"sequence_number\":7}\n\ndata: {\"type\":\"response.output_item.done\",\"output_index\":0,\"item\":{\"id\":\"msg_tmp_qpwvyuytpog\",\"type\":\"message\",\"status\":\"completed\",\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"Hello!\",\"annotations\":[],\"logprobs\":[]}]},\"sequence_number\":8}\n\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"gen-1790738766-AxuIaR7Dk36zBth5HcUa\",\"object\":\"response\",\"created_at\":1790738766,\"model\":\"openai/gpt-4o-mini\",\"status\":\"completed\",\"completed_at\":1790738767,\"output\":[{\"id\":\"msg_tmp_qpwvyuytpog\",\"type\":\"message\",\"role\":\"assistant\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"text\":\"Hello!\",\"annotations\":[],\"logprobs\":[]}]}],\"error\":null,\"incomplete_details\":null,\"tools\":[],\"tool_choice\":\"auto\",\"parallel_tool_calls\":true,\"max_output_tokens\":40,\"temperature\":0,\"top_p\":1,\"presence_penalty\":0,\"frequency_penalty\":0,\"top_logprobs\":0,\"max_tool_calls\":null,\"metadata\":{},\"background\":false,\"previous_response_id\":null,\"service_tier\":\"default\",\"truncation\":\"disabled\",\"store\":false,\"instructions\":\"You are concise.\",\"text\":{\"format\":{\"type\":\"text\"}},\"reasoning\":null,\"safety_identifier\":null,\"prompt_cache_key\":null,\"prompt_cache_options\":null,\"usage\":{\"input_tokens\":21,\"input_tokens_details\":{\"cached_tokens\":0},\"output_tokens\":2,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":23,\"cost\":0.00000435,\"is_byok\":false,\"cost_details\":{\"upstream_inference_cost\":0.00000435,\"upstream_inference_input_cost\":0.00000315,\"upstream_inference_output_cost\":0.0000012}}},\"sequence_number\":9}\n\ndata: [DONE]\n\n"
|
||||
"body": "data: {\"type\":\"response.created\",\"response\":{\"id\":\"gen-1790743725-CVE8eRBKqzoJqJHXU1ur\",\"object\":\"response\",\"created_at\":1790743725,\"model\":\"openai/gpt-4o-mini\",\"status\":\"in_progress\",\"completed_at\":null,\"output\":[],\"error\":null,\"incomplete_details\":null,\"tools\":[],\"tool_choice\":\"auto\",\"parallel_tool_calls\":true,\"max_output_tokens\":40,\"temperature\":0,\"top_p\":1,\"presence_penalty\":0,\"frequency_penalty\":0,\"top_logprobs\":0,\"max_tool_calls\":null,\"metadata\":{},\"background\":false,\"previous_response_id\":null,\"service_tier\":\"auto\",\"truncation\":\"disabled\",\"store\":false,\"instructions\":\"You are concise.\",\"text\":{\"format\":{\"type\":\"text\"}},\"reasoning\":null,\"safety_identifier\":null,\"prompt_cache_key\":null,\"prompt_cache_options\":null,\"usage\":null},\"sequence_number\":0}\n\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"gen-1790743725-CVE8eRBKqzoJqJHXU1ur\",\"object\":\"response\",\"created_at\":1790743725,\"model\":\"openai/gpt-4o-mini\",\"status\":\"in_progress\",\"completed_at\":null,\"output\":[],\"error\":null,\"incomplete_details\":null,\"tools\":[],\"tool_choice\":\"auto\",\"parallel_tool_calls\":true,\"max_output_tokens\":40,\"temperature\":0,\"top_p\":1,\"presence_penalty\":0,\"frequency_penalty\":0,\"top_logprobs\":0,\"max_tool_calls\":null,\"metadata\":{},\"background\":false,\"previous_response_id\":null,\"service_tier\":\"auto\",\"truncation\":\"disabled\",\"store\":false,\"instructions\":\"You are concise.\",\"text\":{\"format\":{\"type\":\"text\"}},\"reasoning\":null,\"safety_identifier\":null,\"prompt_cache_key\":null,\"prompt_cache_options\":null,\"usage\":null},\"sequence_number\":1}\n\ndata: {\"type\":\"response.output_item.added\",\"output_index\":0,\"item\":{\"id\":\"msg_tmp_ilrlnvtd9l9\",\"type\":\"message\",\"status\":\"in_progress\",\"role\":\"assistant\",\"content\":[]},\"sequence_number\":2}\n\ndata: {\"type\":\"response.content_part.added\",\"output_index\":0,\"item_id\":\"msg_tmp_ilrlnvtd9l9\",\"content_index\":0,\"part\":{\"type\":\"output_text\",\"text\":\"\",\"annotations\":[],\"logprobs\":[]},\"sequence_number\":3}\n\ndata: {\"type\":\"response.output_text.delta\",\"output_index\":0,\"item_id\":\"msg_tmp_ilrlnvtd9l9\",\"content_index\":0,\"delta\":\"Hello\",\"logprobs\":[],\"sequence_number\":4}\n\ndata: {\"type\":\"response.output_text.delta\",\"output_index\":0,\"item_id\":\"msg_tmp_ilrlnvtd9l9\",\"content_index\":0,\"delta\":\"!\",\"logprobs\":[],\"sequence_number\":5}\n\ndata: {\"type\":\"response.output_text.done\",\"output_index\":0,\"item_id\":\"msg_tmp_ilrlnvtd9l9\",\"content_index\":0,\"text\":\"Hello!\",\"logprobs\":[],\"sequence_number\":6}\n\ndata: {\"type\":\"response.content_part.done\",\"output_index\":0,\"item_id\":\"msg_tmp_ilrlnvtd9l9\",\"content_index\":0,\"part\":{\"type\":\"output_text\",\"text\":\"Hello!\",\"annotations\":[],\"logprobs\":[]},\"sequence_number\":7}\n\ndata: {\"type\":\"response.output_item.done\",\"output_index\":0,\"item\":{\"id\":\"msg_tmp_ilrlnvtd9l9\",\"type\":\"message\",\"status\":\"completed\",\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"Hello!\",\"annotations\":[],\"logprobs\":[]}]},\"sequence_number\":8}\n\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"gen-1790743725-CVE8eRBKqzoJqJHXU1ur\",\"object\":\"response\",\"created_at\":1790743725,\"model\":\"openai/gpt-4o-mini\",\"status\":\"completed\",\"completed_at\":1790743727,\"output\":[{\"id\":\"msg_tmp_ilrlnvtd9l9\",\"type\":\"message\",\"role\":\"assistant\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"text\":\"Hello!\",\"annotations\":[],\"logprobs\":[]}]}],\"error\":null,\"incomplete_details\":null,\"tools\":[],\"tool_choice\":\"auto\",\"parallel_tool_calls\":true,\"max_output_tokens\":40,\"temperature\":0,\"top_p\":1,\"presence_penalty\":0,\"frequency_penalty\":0,\"top_logprobs\":0,\"max_tool_calls\":null,\"metadata\":{},\"background\":false,\"previous_response_id\":null,\"service_tier\":\"default\",\"truncation\":\"disabled\",\"store\":false,\"instructions\":\"You are concise.\",\"text\":{\"format\":{\"type\":\"text\"}},\"reasoning\":null,\"safety_identifier\":null,\"prompt_cache_key\":null,\"prompt_cache_options\":null,\"usage\":{\"input_tokens\":21,\"input_tokens_details\":{\"cached_tokens\":0},\"output_tokens\":2,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":23,\"cost\":0.00000435,\"is_byok\":false,\"cost_details\":{\"upstream_inference_cost\":0.00000435,\"upstream_inference_input_cost\":0.00000315,\"upstream_inference_output_cost\":0.0000012}}},\"sequence_number\":9}\n\ndata: [DONE]\n\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Vendored
+2
-2
File diff suppressed because one or more lines are too long
Vendored
+4
-4
File diff suppressed because one or more lines are too long
Vendored
+4
-4
File diff suppressed because one or more lines are too long
Vendored
+5
-5
File diff suppressed because one or more lines are too long
Vendored
+5
-5
File diff suppressed because one or more lines are too long
@@ -8,7 +8,7 @@ import { expectWeatherToolLoop, goldenWeatherToolLoopRequest, runWeatherToolLoop
|
||||
const recorded = recordedTests({
|
||||
prefix: "openrouter-responses",
|
||||
provider: "openrouter",
|
||||
protocol: "openrouter-meta-responses",
|
||||
protocol: "openrouter-responses",
|
||||
requires: ["OPENROUTER_API_KEY"],
|
||||
})
|
||||
|
||||
|
||||
@@ -9,31 +9,51 @@ import { fixedResponse } from "../lib/http.js"
|
||||
import { sseEvents } from "../lib/sse.js"
|
||||
|
||||
describe("OpenRouter", () => {
|
||||
it.effect("routes OpenAI, xAI, and Meta models through their native Responses protocols", () =>
|
||||
Effect.forEach(
|
||||
[
|
||||
["openai/gpt-4o-mini", "openrouter-responses"],
|
||||
["x-ai/grok-4.3", "openrouter-xai-responses"],
|
||||
["meta/muse-spark-1.3", "openrouter-meta-responses"],
|
||||
] as const,
|
||||
([id, route]) =>
|
||||
Effect.gen(function* () {
|
||||
const model = OpenRouter.configure({ apiKey: "test-key" }).model(id)
|
||||
const prepared = yield* compileRequest(LLM.request({ model, prompt: "Say hello." }))
|
||||
it.effect("routes OpenAI, xAI, and Meta models through the same generic Responses protocol", () =>
|
||||
Effect.forEach(["openai/gpt-4o-mini", "x-ai/grok-4.3", "meta/muse-spark-1.3", "~x-ai/grok-latest"], (id) =>
|
||||
Effect.gen(function* () {
|
||||
const model = OpenRouter.configure({
|
||||
apiKey: "test-key",
|
||||
providerOptions: {
|
||||
reasoning: { effort: "low", exclude: true },
|
||||
text: { verbosity: "low" },
|
||||
provider: { allow_fallbacks: false },
|
||||
},
|
||||
}).model(id)
|
||||
const prepared = yield* compileRequest(LLM.request({ model, prompt: "Say hello." }))
|
||||
|
||||
expect(model.route.endpoint.baseURL).toBe("https://openrouter.ai/api/v1")
|
||||
expect(prepared.route).toBe(route)
|
||||
expect(prepared.body).toMatchObject({
|
||||
model: id,
|
||||
input: [{ role: "user", content: [{ type: "input_text", text: "Say hello." }] }],
|
||||
stream: true,
|
||||
store: false,
|
||||
include: ["reasoning.encrypted_content"],
|
||||
})
|
||||
}),
|
||||
expect(model.route.endpoint.baseURL).toBe("https://openrouter.ai/api/v1")
|
||||
expect(prepared.route).toBe("openrouter-responses")
|
||||
expect(model.route.protocol).toBe(OpenRouter.responses("openai/gpt-4o-mini").route.protocol)
|
||||
expect(prepared.body).toMatchObject({
|
||||
model: id,
|
||||
input: [{ role: "user", content: [{ type: "input_text", text: "Say hello." }] }],
|
||||
stream: true,
|
||||
store: false,
|
||||
include: ["reasoning.encrypted_content"],
|
||||
reasoning: { effort: "low", exclude: true },
|
||||
text: { verbosity: "low" },
|
||||
provider: { allow_fallbacks: false },
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
it.effect("exposes exactly three API routes and leaves other families on Chat", () =>
|
||||
Effect.gen(function* () {
|
||||
expect(OpenRouter.routes.map((route) => route.endpoint.path)).toEqual([
|
||||
"/chat/completions",
|
||||
"/responses",
|
||||
"/messages",
|
||||
])
|
||||
const openrouter = OpenRouter.configure({ apiKey: "test-key" })
|
||||
const chat = openrouter.model("meta-llama/llama-3.3-70b-instruct")
|
||||
expect(chat.route.id).toBe("openrouter")
|
||||
expect(openrouter.model("qwen/qwen3-coder-plus").route.protocol).toBe(chat.route.protocol)
|
||||
expect(openrouter.messages("anthropic/claude-sonnet-4.6").route.id).toBe("openrouter-messages")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("places default cache breakpoints on tools, system boundaries, and the conversation tail", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
|
||||
Reference in New Issue
Block a user