refactor(ai): share OpenRouter Responses protocol

This commit is contained in:
Aiden Cline
2026-09-29 23:50:21 -05:00
parent 4ec4f274bd
commit 25d302d02c
15 changed files with 112 additions and 123 deletions
+4 -2
View File
@@ -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
+1 -1
View File
@@ -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])),
+1 -1
View File
@@ -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),
})
+19 -3
View File
@@ -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,
+1 -1
View File
@@ -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),
+21 -70
View File
@@ -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) =>
@@ -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"
}
}
]
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
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"],
})
+41 -21
View File
@@ -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(