Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions runner/MODEL-VALIDATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ Primary provider documentation checked on 2026-09-13:

The record pins the Groq upstream through OpenRouter with `only: ["groq"]`, `allow_fallbacks: false` and `require_parameters: true`. The endpoint metadata reports 131,072 context tokens, 117,964 maximum completion tokens and tool support for that upstream. Other upstreams have different limits; changing upstream requires a new configuration ID. Authentication and billing go through OpenRouter; no Groq key is required. The original direct-Groq record remains available unchanged.

The pinned SDK's OpenRouter serializer inherits OpenAI-specific request fields. The runner adapts its serialized body to send `max_tokens` and omit `store` and `prompt_cache_key`, which the selected endpoint does not advertise. Strict parameter matching remains enabled. The HTTP transport test checks the actual outgoing body for initial turns, tool continuations and local compaction.

Provider documentation establishes advertised support. It does not prove the pinned native transport works with the account or model. The live check is still a release prerequisite.

## Limits and compaction
Expand Down
29 changes: 27 additions & 2 deletions runner/src/ModelConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
// catalog or fixture metadata.
import { Duration, Effect, Layer, Redacted, Schema, Stream } from "effect"
import { HttpClient, HttpClientError } from "effect/unstable/http"
import { Auth } from "@opencode/ai/route"
import { Auth, Route } from "@opencode/ai/route"
import { OpenAIChat } from "@opencode/ai/protocols"
import * as OpenRouter from "@opencode/ai/providers/openrouter"
import { SessionRunnerModel } from "@opencode/core/session/runner/model"
Expand Down Expand Up @@ -126,9 +126,34 @@ const credentialFor = (record: ModelConfigurationRecord, secrets: SecretReader)
}),
)

// The pinned SDK inherits OpenAI-only fields in its OpenRouter serializer.
// Strict OpenRouter routing rejects those fields for the selected upstream.
const openRouterRoute = Route.make({
id: "openrouter",
provider: OpenRouter.id,
providerMetadataKey: "openrouter",
endpoint: OpenRouter.route.endpoint,
framing: OpenAIChat.framing,
protocol: {
...OpenRouter.protocol,
body: {
...OpenRouter.protocol.body,
from: (request): ReturnType<typeof OpenRouter.protocol.body.from> =>
OpenRouter.protocol.body.from(request).pipe(
Effect.map(
({ store: _store, prompt_cache_key: _cacheKey, max_completion_tokens, ...body }) => ({
...body,
...(max_completion_tokens === undefined ? {} : { max_tokens: max_completion_tokens }),
}),
),
),
},
},
})

/** Builds the native route model for a record. Secrets resolve lazily per request. */
export const languageModelFor = (record: ModelConfigurationRecord, secrets: SecretReader) => {
const route = (record.route === "openrouter" ? OpenRouter.route : OpenAIChat.route).with({
const route = (record.route === "openrouter" ? openRouterRoute : OpenAIChat.route).with({
provider: record.provider,
endpoint: { baseURL: record.endpoint },
auth: Auth.bearer(credentialFor(record, secrets)),
Expand Down
5 changes: 5 additions & 0 deletions runner/test/ModelProvider.live.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ const validate = async (live: boolean) => {
outboundService: async (request) => {
const body = await request.text()
const parsed = JSON.parse(body)
// OpenRouter strict routing must not exclude Groq with OpenAI-only options.
expect(parsed).not.toHaveProperty("store")
expect(parsed).not.toHaveProperty("prompt_cache_key")
expect(parsed).not.toHaveProperty("max_completion_tokens")
expect(parsed.max_tokens).toBe(2048)
// Every retry and compaction request spends this same allowance. No body rewrite.
if (
++requests > 8 ||
Expand Down