From 134a71cd664373562559be34fa7ce1b564beb0ef Mon Sep 17 00:00:00 2001 From: Sawyer Date: Wed, 9 Sep 2026 21:21:15 -0700 Subject: [PATCH 1/6] Add failing Grok stream-terminal oracle --- src/provider/grok-responses-adapter.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/provider/grok-responses-adapter.test.ts b/src/provider/grok-responses-adapter.test.ts index 307291088..4744c69c1 100644 --- a/src/provider/grok-responses-adapter.test.ts +++ b/src/provider/grok-responses-adapter.test.ts @@ -196,4 +196,18 @@ describe("createGrokResponsesAdapter", () => { ).toBe(1_500); expect(adapter.extractRetryAfterMs?.(new Headers({}))).toBeUndefined(); }); + + test("treats Responses completed, incomplete, and done events as stream-terminal", () => { + const adapter = createGrokResponsesAdapter(source); + expect(typeof adapter.isStreamTerminal).toBe("function"); + for (const type of ["response.completed", "response.incomplete", "response.done"]) { + expect(adapter.isStreamTerminal!(JSON.stringify({ type }))).toBe(true); + } + for (const type of ["response.output_text.delta", "response.created", "response.in_progress"]) { + expect(adapter.isStreamTerminal!(JSON.stringify({ type }))).toBe(false); + } + expect(adapter.isStreamTerminal!("{not json")).toBe(false); + expect(adapter.isStreamTerminal!("null")).toBe(false); + expect(adapter.isStreamTerminal!('"just a string"')).toBe(false); + }); }); From f70c3071612a5a5fabc6ca832cecf4c5ed30edcc Mon Sep 17 00:00:00 2001 From: Sawyer Date: Wed, 9 Sep 2026 21:22:48 -0700 Subject: [PATCH 2/6] Add openai-responses and xai-provider git dependencies --- bun.lock | 9 +++++++++ package.json | 14 ++++++++------ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/bun.lock b/bun.lock index c961244f3..090174961 100644 --- a/bun.lock +++ b/bun.lock @@ -5,6 +5,8 @@ "": { "name": "@corbits/code", "dependencies": { + "@corbits/openai-responses": "github:corbitsdev/corbits-openai-responses", + "@corbits/xai-provider": "github:corbitsdev/corbits-xai-provider", "@intx/agent": "workspace:*", "@intx/authz": "workspace:*", "@intx/crypto": "0.3.0", @@ -205,8 +207,15 @@ "@corbits/first-class-providers": ["@corbits/first-class-providers@workspace:packages/first-class-providers"], + "@corbits/oauth-core": ["@corbits/oauth-core@github:corbitsdev/corbits-oauth-core#e1e69e6", { "dependencies": { "arktype": "2.2.3" } }, "corbitsdev-corbits-oauth-core-e1e69e6", "sha512-wUrD73iVyk/Dtb4yRn3hCh6N8syfsEvAkRz4XYqT0FIB3sYDSMYEWHcDMJx9nq64my/HBOeN1J6PHIOtpjhtPg=="], + + "@corbits/openai-responses": ["@corbits/openai-responses@github:corbitsdev/corbits-openai-responses#1d20dbb", { "dependencies": { "arktype": "2.2.3" }, "peerDependencies": { "@intx/inference": ">=0.3.0", "@intx/types": ">=0.3.0" } }, "corbitsdev-corbits-openai-responses-1d20dbb", "sha512-lSJz1KkKD8mEUYiQ57dJprD9JavdJGt3OBRa42tvXLsikt1fJ6qX2mGCwaEXzkfZNc2XY5o7ZpxK5swSJX+LnA=="], + "@corbits/provider-opencode-go": ["@corbits/provider-opencode-go@workspace:packages/opencode-go"], + "@corbits/xai-provider": ["@corbits/xai-provider@github:corbitsdev/corbits-xai-provider#cadf9b9", { "dependencies": { "@corbits/oauth-core": "github:corbitsdev/corbits-oauth-core", "@corbits/openai-responses": "github:corbitsdev/corbits-openai-responses", "arktype": "2.2.3" }, "peerDependencies": { "@intx/inference": ">=0.3.0", "@intx/types": ">=0.3.0" } }, "corbitsdev-corbits-xai-provider-cadf9b9", "sha512-dVY4lQJWdCVPxwnK4pQNc3JgfgASaNj/GACKTnR22Y/s6CKSwlcZyGzZ0QEjIr21SZRk4+QP6ehOBf/OtHSJRw=="], + + "@gar/promise-retry": ["@gar/promise-retry@1.0.3", "", {}, "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA=="], "@hono/node-server": ["@hono/node-server@2.1.1", "", { "peerDependencies": { "hono": "^4" } }, "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg=="], diff --git a/package.json b/package.json index 353b51131..462be039f 100644 --- a/package.json +++ b/package.json @@ -80,21 +80,23 @@ "tar": "^7.5.1" }, "dependencies": { + "@corbits/openai-responses": "github:corbitsdev/corbits-openai-responses", + "@corbits/xai-provider": "github:corbitsdev/corbits-xai-provider", "@intx/agent": "workspace:*", "@intx/authz": "workspace:*", "@intx/crypto": "0.3.0", + "@intx/harness": "workspace:*", + "@intx/hub-sessions": "0.3.0", "@intx/inference": "workspace:*", "@intx/log": "workspace:*", + "@intx/mail-memory": "0.3.0", + "@intx/mailbox": "workspace:*", + "@intx/mime": "workspace:*", "@intx/storage-isogit": "workspace:*", "@intx/tools-lsp": "0.3.0", "@intx/tools-posix": "workspace:*", - "@intx/mailbox": "workspace:*", - "@intx/harness": "workspace:*", - "@intx/mime": "workspace:*", - "@intx/mail-memory": "0.3.0", - "@intx/hub-sessions": "0.3.0", - "@intx/workflow-host": "0.3.0", "@intx/types": "workspace:*", + "@intx/workflow-host": "0.3.0", "@modelcontextprotocol/sdk": "^1.29.0", "@opentui/core": "0.5.10", "arktype": "catalog:", From 08ad8fed385fc5851b8ccef50bb46f65966bc9ff Mon Sep 17 00:00:00 2001 From: Sawyer Date: Wed, 9 Sep 2026 21:23:36 -0700 Subject: [PATCH 3/6] Hoist the OpenCode session option helper --- src/config/index.ts | 2 +- src/provider/openai-responses-adapter.ts | 10 +--------- src/provider/opencode-go-adapter.ts | 5 +---- src/provider/opencode-go-anthropic-adapter.ts | 5 +---- src/provider/opencode-session.ts | 8 ++++++++ tests/unit/openai-responses-adapter.test.ts | 2 +- 6 files changed, 13 insertions(+), 19 deletions(-) create mode 100644 src/provider/opencode-session.ts diff --git a/src/config/index.ts b/src/config/index.ts index d396957ac..7ee939b5a 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -53,8 +53,8 @@ import { selectableGoModelIds } from "../provider/opencode-go-models.js"; import { OPENAI_RESPONSES_PROVIDER, OPENAI_SESSION_ID_OPTION, - OPENCODE_SESSION_ID_OPTION, } from "../provider/openai-responses-adapter.js"; +import { OPENCODE_SESSION_ID_OPTION } from "../provider/opencode-session.js"; import { OPENCODE_GO_MESSAGES_PROVIDER } from "../provider/opencode-go-anthropic-adapter.js"; import { xaiUserIdFromAccessToken } from "../auth/xai/session.js"; import { diff --git a/src/provider/openai-responses-adapter.ts b/src/provider/openai-responses-adapter.ts index 1cdd07bf5..327d85167 100644 --- a/src/provider/openai-responses-adapter.ts +++ b/src/provider/openai-responses-adapter.ts @@ -19,6 +19,7 @@ import { parseResponse, signatureForModel, } from "./codex-responses-adapter.js"; +import { OPENCODE_SESSION_ID_OPTION, optionString } from "./opencode-session.js"; // Generic OpenAI Responses API adapter (POST /responses). Used by OpenCode Go // models that speak Responses rather than Chat Completions (e.g. gpt-5.6-luna). @@ -29,7 +30,6 @@ export const OPENAI_RESPONSES_PROVIDER = "openai-responses"; // Key the source stashes in defaults.providerOptions for this adapter. export const OPENAI_SESSION_ID_OPTION = "openaiSessionId"; -export const OPENCODE_SESSION_ID_OPTION = "opencodeSessionId"; type ResponsesInputContentPart = | { type: "input_text"; text: string } @@ -160,14 +160,6 @@ function toResponsesTools(options: InferenceOptions): unknown[] | undefined { })); } -export function optionString( - options: InferenceOptions, - key: string, -): string | undefined { - const value = options.providerOptions?.[key]; - return typeof value === "string" && value.length > 0 ? value : undefined; -} - // Keeps the LAST occurrence of each duplicate function_call / function_call_output // call_id, not the first: a duplicate is most often a corrected retry, and // discarding the retry in favor of the stale original silently replays the diff --git a/src/provider/opencode-go-adapter.ts b/src/provider/opencode-go-adapter.ts index 047be5b6a..1d072b776 100644 --- a/src/provider/opencode-go-adapter.ts +++ b/src/provider/opencode-go-adapter.ts @@ -1,9 +1,6 @@ import type { BuiltRequest, ProviderAdapter } from "@intx/inference"; import { createOpenAICompatibleAdapter } from "./openai-compatible-adapter.js"; -import { - OPENCODE_SESSION_ID_OPTION, - optionString, -} from "./openai-responses-adapter.js"; +import { OPENCODE_SESSION_ID_OPTION, optionString } from "./opencode-session.js"; type AdapterSource = Parameters[0]; diff --git a/src/provider/opencode-go-anthropic-adapter.ts b/src/provider/opencode-go-anthropic-adapter.ts index 3a637411a..3c8090b50 100644 --- a/src/provider/opencode-go-anthropic-adapter.ts +++ b/src/provider/opencode-go-anthropic-adapter.ts @@ -1,9 +1,6 @@ import { type BuiltRequest, type ProviderAdapter } from "@intx/inference"; import { createAnthropicAdapter } from "@intx/inference/providers"; -import { - OPENCODE_SESSION_ID_OPTION, - optionString, -} from "./openai-responses-adapter.js"; +import { OPENCODE_SESSION_ID_OPTION, optionString } from "./opencode-session.js"; export const OPENCODE_GO_MESSAGES_PROVIDER = "opencode-go-messages"; diff --git a/src/provider/opencode-session.ts b/src/provider/opencode-session.ts new file mode 100644 index 000000000..48f1c06c4 --- /dev/null +++ b/src/provider/opencode-session.ts @@ -0,0 +1,8 @@ +import type { InferenceOptions } from "@intx/types/runtime"; + +export const OPENCODE_SESSION_ID_OPTION = "opencodeSessionId"; + +export function optionString(options: InferenceOptions, key: string): string | undefined { + const value = options.providerOptions?.[key]; + return typeof value === "string" && value.length > 0 ? value : undefined; +} diff --git a/tests/unit/openai-responses-adapter.test.ts b/tests/unit/openai-responses-adapter.test.ts index 75025a05f..4e721c7ae 100644 --- a/tests/unit/openai-responses-adapter.test.ts +++ b/tests/unit/openai-responses-adapter.test.ts @@ -2,8 +2,8 @@ import { test, expect, describe } from "bun:test"; import { createOpenAIResponsesAdapter, OPENAI_SESSION_ID_OPTION, - OPENCODE_SESSION_ID_OPTION, } from "../../src/provider/openai-responses-adapter.js"; +import { OPENCODE_SESSION_ID_OPTION } from "../../src/provider/opencode-session.js"; import { BEARER_CREDENTIAL_SENTINEL } from "@intx/inference"; import type { ConversationTurn, From 45cba51ce290947de5f57f443a54fdea644c4743 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Wed, 9 Sep 2026 21:24:46 -0700 Subject: [PATCH 4/6] Route OpenCode Go Responses through the shared factory --- src/config/index.ts | 2 +- src/provider/inference-dependencies.ts | 8 +- src/provider/openai-responses-adapter.ts | 254 -------------------- src/provider/openai-responses.ts | 22 ++ tests/unit/openai-responses-adapter.test.ts | 2 +- 5 files changed, 28 insertions(+), 260 deletions(-) delete mode 100644 src/provider/openai-responses-adapter.ts create mode 100644 src/provider/openai-responses.ts diff --git a/src/config/index.ts b/src/config/index.ts index 7ee939b5a..0a7003cfa 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -53,7 +53,7 @@ import { selectableGoModelIds } from "../provider/opencode-go-models.js"; import { OPENAI_RESPONSES_PROVIDER, OPENAI_SESSION_ID_OPTION, -} from "../provider/openai-responses-adapter.js"; +} from "../provider/openai-responses.js"; import { OPENCODE_SESSION_ID_OPTION } from "../provider/opencode-session.js"; import { OPENCODE_GO_MESSAGES_PROVIDER } from "../provider/opencode-go-anthropic-adapter.js"; import { xaiUserIdFromAccessToken } from "../auth/xai/session.js"; diff --git a/src/provider/inference-dependencies.ts b/src/provider/inference-dependencies.ts index 5f3062bda..4cdf93fd2 100644 --- a/src/provider/inference-dependencies.ts +++ b/src/provider/inference-dependencies.ts @@ -9,7 +9,7 @@ import * as opencodeGo from "./opencode-go-adapter.js"; import * as codexResponses from "./codex-responses-adapter.js"; import * as grokResponses from "./grok-responses-adapter.js"; import * as bifrostAdapter from "./bifrost-adapter.js"; -import * as openaiResponses from "./openai-responses-adapter.js"; +import * as openaiResponses from "./openai-responses.js"; import * as opencodeGoAnthropic from "./opencode-go-anthropic-adapter.js"; import { CODEX_RESPONSES_PROVIDER, @@ -19,7 +19,7 @@ import { GROK_RESPONSES_PROVIDER } from "./grok-responses-adapter.js"; import { withReplaySanitizer } from "./replay-sanitizer.js"; import { OPENCODE_GO_PROVIDER_ID } from "../../packages/opencode-go/src/index.js"; import { BIFROST_PROVIDER } from "./bifrost-adapter.js"; -import { OPENAI_RESPONSES_PROVIDER } from "./openai-responses-adapter.js"; +import { OPENAI_RESPONSES_PROVIDER } from "./openai-responses.js"; import { OPENCODE_GO_MESSAGES_PROVIDER } from "./opencode-go-anthropic-adapter.js"; // Corbits Code ships first-party adapters on top of the built-in provider set: @@ -54,7 +54,7 @@ const manifest: AdapterManifest = [ }, { provider: OPENAI_RESPONSES_PROVIDER, - specifier: "openai-responses-adapter", + specifier: "openai-responses", export: "createOpenAIResponsesAdapter", }, { @@ -70,7 +70,7 @@ const localModules: Record = { "codex-responses-adapter": codexResponses, "grok-responses-adapter": grokResponses, "bifrost-adapter": bifrostAdapter, - "openai-responses-adapter": openaiResponses, + "openai-responses": openaiResponses, "opencode-go-anthropic-adapter": opencodeGoAnthropic, }; diff --git a/src/provider/openai-responses-adapter.ts b/src/provider/openai-responses-adapter.ts deleted file mode 100644 index 327d85167..000000000 --- a/src/provider/openai-responses-adapter.ts +++ /dev/null @@ -1,254 +0,0 @@ -import { - BEARER_CREDENTIAL_SENTINEL, - encodeToolName, - type BuiltRequest, - type ProviderAdapter, -} from "@intx/inference"; -import type { - ContentBlock, - ConversationTurn, - InferenceOptions, - LastCycleSource, -} from "@intx/types/runtime"; -import { - RESPONSES_TOOL_NAME_LIMIT, - createResponsesBlockIndexer, - extractResponsesRetryAfterMs, - isResponsesStreamTerminal, - parseJSONResponse, - parseResponse, - signatureForModel, -} from "./codex-responses-adapter.js"; -import { OPENCODE_SESSION_ID_OPTION, optionString } from "./opencode-session.js"; - -// Generic OpenAI Responses API adapter (POST /responses). Used by OpenCode Go -// models that speak Responses rather than Chat Completions (e.g. gpt-5.6-luna). -// Shares the Codex/Grok SSE parser; only the request shape and path differ from -// Chat Completions and from the Grok-specific header set. - -export const OPENAI_RESPONSES_PROVIDER = "openai-responses"; - -// Key the source stashes in defaults.providerOptions for this adapter. -export const OPENAI_SESSION_ID_OPTION = "openaiSessionId"; - -type ResponsesInputContentPart = - | { type: "input_text"; text: string } - | { type: "input_image"; image_url: string }; - -type ResponsesInputItem = - | { - type: "message"; - role: "user" | "assistant" | "system"; - content: string | ResponsesInputContentPart[]; - } - | { type: "function_call"; name: string; arguments: string; call_id: string } - | { type: "function_call_output"; call_id: string; output: string } - | { type: "reasoning"; summary: never[]; encrypted_content: string }; - -function toolResultText( - block: Extract, -): string { - const parts: string[] = []; - for (const c of block.content) { - if (c.type === "text") parts.push(c.text); - else parts.push(`[unsupported ${c.type} content omitted]`); - } - return parts.join(""); -} - -function toResponsesItems( - turn: ConversationTurn, - requestModel: string, - requestProvider: string, -): ResponsesInputItem[] { - const items: ResponsesInputItem[] = []; - const role = turn.role; - const parts: ResponsesInputContentPart[] = []; - let hasImage = false; - // See codex-responses-adapter.ts toResponsesItems for why an unreplayed - // reasoning item suppresses the function_call(s) it produced. - let suppressOrphanedCalls = false; - - const flushMessage = (): void => { - if (parts.length === 0) return; - items.push({ - type: "message", - role, - content: hasImage - ? [...parts] - : parts - .map((part) => (part.type === "input_text" ? part.text : "")) - .join(""), - }); - parts.length = 0; - hasImage = false; - suppressOrphanedCalls = false; - }; - - for (const block of turn.content) { - if (block.type === "text") { - parts.push({ type: "input_text", text: block.text }); - } else if (block.type === "image") { - if (block.source.kind === "base64") { - hasImage = true; - parts.push({ - type: "input_image", - image_url: `data:${block.source.mimeType};base64,${block.source.data}`, - }); - } else if (block.source.kind === "url") { - hasImage = true; - parts.push({ type: "input_image", image_url: block.source.url }); - } else { - parts.push({ - type: "input_text", - text: `[Unsupported image reference omitted: ${block.source.reference}]`, - }); - } - } else if (block.type === "tool_call") { - if (suppressOrphanedCalls) continue; - flushMessage(); - items.push({ - type: "function_call", - name: encodeToolName(block.name, RESPONSES_TOOL_NAME_LIMIT), - arguments: JSON.stringify(block.arguments ?? {}), - call_id: block.id, - }); - } else if (block.type === "tool_result") { - flushMessage(); - suppressOrphanedCalls = false; - items.push({ - type: "function_call_output", - call_id: block.callId, - output: toolResultText(block), - }); - } else if ( - block.type === "thinking" && - typeof block.signature === "string" && - block.signature.length > 0 - ) { - flushMessage(); - const encryptedContent = signatureForModel( - turn, - requestModel, - requestProvider, - block.signature, - ); - if (encryptedContent !== undefined) { - items.push({ - type: "reasoning", - summary: [], - encrypted_content: encryptedContent, - }); - suppressOrphanedCalls = false; - } else { - suppressOrphanedCalls = true; - } - } - } - flushMessage(); - return items; -} - -function toResponsesTools(options: InferenceOptions): unknown[] | undefined { - if (options.tools === undefined || options.tools.length === 0) - return undefined; - return options.tools.map((t) => ({ - type: "function", - name: encodeToolName(t.name, RESPONSES_TOOL_NAME_LIMIT), - description: t.description, - parameters: t.inputSchema, - })); -} - -// Keeps the LAST occurrence of each duplicate function_call / function_call_output -// call_id, not the first: a duplicate is most often a corrected retry, and -// discarding the retry in favor of the stale original silently replays the -// wrong tool result. Both item types are covered — a duplicated function_call -// is just as invalid on the wire as a duplicated output. -function dedupeToolItems(items: ResponsesInputItem[]): ResponsesInputItem[] { - const lastIndexForCall = new Map(); - items.forEach((item, i) => { - if (item.type === "function_call" || item.type === "function_call_output") { - lastIndexForCall.set(`${item.type}:${item.call_id}`, i); - } - }); - return items.filter((item, i) => { - if (item.type === "function_call" || item.type === "function_call_output") { - return lastIndexForCall.get(`${item.type}:${item.call_id}`) === i; - } - return true; - }); -} - -function buildRequest( - messages: ConversationTurn[], - model: string, - options: InferenceOptions, - requestProvider: string, -): BuiltRequest { - const conversation = dedupeToolItems( - messages.flatMap((turn) => toResponsesItems(turn, model, requestProvider)), - ); - const systemMessage: ResponsesInputItem | undefined = - options.systemPrompt !== undefined - ? { type: "message", role: "system", content: options.systemPrompt } - : undefined; - const input = - systemMessage !== undefined - ? [systemMessage, ...conversation] - : conversation; - const tools = toResponsesTools(options); - - const body: Record = { - model, - input, - store: false, - stream: true, - include: ["reasoning.encrypted_content"], - reasoning: { summary: "auto" }, - }; - if (tools !== undefined) { - body["tools"] = tools; - body["tool_choice"] = "auto"; - } - if (options.maxTokens !== undefined) - body["max_output_tokens"] = options.maxTokens; - if (options.temperature !== undefined) - body["temperature"] = options.temperature; - // With store:false this is the only cache-routing signal; keying it to the - // inference thread's session id keeps every request on the same cache shard. - const sessionId = optionString(options, OPENAI_SESSION_ID_OPTION); - if (sessionId !== undefined) body["prompt_cache_key"] = sessionId; - const opencodeSessionId = optionString(options, OPENCODE_SESSION_ID_OPTION); - - return { - url: "/responses", - headers: { - "content-type": "application/json", - accept: "text/event-stream", - authorization: BEARER_CREDENTIAL_SENTINEL, - ...(opencodeSessionId !== undefined - ? { "x-opencode-session": opencodeSessionId } - : {}), - }, - body: JSON.stringify(body), - }; -} - -export function createOpenAIResponsesAdapter( - source: LastCycleSource, -): ProviderAdapter { - // Re-created per request in buildRequest — see codex-responses-adapter.ts. - let indexer = createResponsesBlockIndexer(); - return { - buildRequest: (messages, model, options) => { - indexer = createResponsesBlockIndexer(); - return buildRequest(messages, model, options, source.provider); - }, - parseResponse: (sseData) => - parseResponse(sseData, indexer, source, OPENAI_RESPONSES_PROVIDER), - parseJSONResponse, - isStreamTerminal: isResponsesStreamTerminal, - extractRetryAfterMs: extractResponsesRetryAfterMs, - }; -} diff --git a/src/provider/openai-responses.ts b/src/provider/openai-responses.ts new file mode 100644 index 000000000..8dd65f6cb --- /dev/null +++ b/src/provider/openai-responses.ts @@ -0,0 +1,22 @@ +import { responsesAdapterFactory, type ResponsesQuirks } from "@corbits/openai-responses"; +import type { AdapterFactory } from "@intx/inference"; +import { OPENCODE_SESSION_ID_OPTION } from "./opencode-session.js"; + +export const OPENAI_RESPONSES_PROVIDER = "openai-responses"; + +export const OPENAI_SESSION_ID_OPTION = "openaiSessionId"; + +// Baked at the host so OpenCode Go Responses does not read source.quirks +// (package createOpenAIResponsesAdapter would, and that is not the current Go wire). +const hostQuirks: ResponsesQuirks = { + path: "/responses", + sessionIdOption: OPENAI_SESSION_ID_OPTION, + headers: { + fromOption: [{ optionKey: OPENCODE_SESSION_ID_OPTION, header: "x-opencode-session" }], + }, + systemPrompt: { role: "system", shape: "string" }, + contentShape: "flat", + reasoning: { summary: "auto" }, +}; + +export const createOpenAIResponsesAdapter: AdapterFactory = responsesAdapterFactory(hostQuirks); diff --git a/tests/unit/openai-responses-adapter.test.ts b/tests/unit/openai-responses-adapter.test.ts index 4e721c7ae..9be47495c 100644 --- a/tests/unit/openai-responses-adapter.test.ts +++ b/tests/unit/openai-responses-adapter.test.ts @@ -2,7 +2,7 @@ import { test, expect, describe } from "bun:test"; import { createOpenAIResponsesAdapter, OPENAI_SESSION_ID_OPTION, -} from "../../src/provider/openai-responses-adapter.js"; +} from "../../src/provider/openai-responses.js"; import { OPENCODE_SESSION_ID_OPTION } from "../../src/provider/opencode-session.js"; import { BEARER_CREDENTIAL_SENTINEL } from "@intx/inference"; import type { From 7f60a43a0627886a4cc62853fb121ba570206bba Mon Sep 17 00:00:00 2001 From: Sawyer Date: Wed, 9 Sep 2026 21:25:50 -0700 Subject: [PATCH 5/6] Re-bake the xAI Responses adapter with host Grok option keys --- src/config/index.ts | 2 +- src/provider/grok-responses-adapter.ts | 278 ------------------ ...adapter.test.ts => grok-responses.test.ts} | 18 +- src/provider/grok-responses.ts | 23 ++ src/provider/inference-dependencies.ts | 8 +- src/provider/replay-sanitizer.test.ts | 2 +- src/subagent/provider-family.ts | 2 +- tests/unit/codex-responses-adapter.test.ts | 2 +- tests/unit/grok-responses-adapter.test.ts | 2 +- 9 files changed, 43 insertions(+), 294 deletions(-) delete mode 100644 src/provider/grok-responses-adapter.ts rename src/provider/{grok-responses-adapter.test.ts => grok-responses.test.ts} (91%) create mode 100644 src/provider/grok-responses.ts diff --git a/src/config/index.ts b/src/config/index.ts index 0a7003cfa..d42fcf50c 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -46,7 +46,7 @@ import { GROK_RESPONSES_PROVIDER, GROK_SESSION_ID_OPTION, GROK_USER_ID_OPTION, -} from "../provider/grok-responses-adapter.js"; +} from "../provider/grok-responses.js"; import { BIFROST_PROVIDER } from "../provider/bifrost-adapter.js"; import { isOllamaProviderId, ollamaOpenAIBaseURL } from "../provider/ollama.js"; import { selectableGoModelIds } from "../provider/opencode-go-models.js"; diff --git a/src/provider/grok-responses-adapter.ts b/src/provider/grok-responses-adapter.ts deleted file mode 100644 index ef65cd09d..000000000 --- a/src/provider/grok-responses-adapter.ts +++ /dev/null @@ -1,278 +0,0 @@ -import { - BEARER_CREDENTIAL_SENTINEL, - encodeToolName, - type BuiltRequest, - type ProviderAdapter, -} from "@intx/inference"; -import type { - ContentBlock, - ConversationTurn, - InferenceOptions, - LastCycleSource, -} from "@intx/types/runtime"; -import { - XAI_RESPONSES_PATH, - XAI_CLIENT_IDENTIFIER, - XAI_CLIENT_VERSION, - XAI_USER_AGENT, -} from "../auth/xai/constants.js"; -import { - RESPONSES_TOOL_NAME_LIMIT, - createResponsesBlockIndexer, - extractResponsesRetryAfterMs, - parseJSONResponse, - parseResponse, - signatureForModel, -} from "./codex-responses-adapter.js"; - -// Adapter for the grok-cli OAuth proxy (cli-chat-proxy.grok.com), which serves -// the OpenAI Responses API at /v1/responses. The request shape mirrors the grok -// CLI's own /v1/responses call (captured live): the system prompt rides as a -// leading `system` input message (string content, not parts), reasoning is -// requested by summary, and the caller is identified by x-grok-* headers rather -// than a body field. The Responses SSE protocol is identical to Codex, so the -// stream parser is shared. - -export const GROK_RESPONSES_PROVIDER = "grok-responses"; - -// Keys the source stashes in defaults.providerOptions for this adapter. -export const GROK_USER_ID_OPTION = "grokUserId"; -export const GROK_SESSION_ID_OPTION = "grokSessionId"; - -type ResponsesInputContentPart = - | { type: "input_text"; text: string } - | { type: "input_image"; image_url: string }; - -type ResponsesInputItem = - | { - type: "message"; - role: "user" | "assistant" | "system"; - content: string | ResponsesInputContentPart[]; - } - | { type: "function_call"; name: string; arguments: string; call_id: string } - | { type: "function_call_output"; call_id: string; output: string } - | { type: "reasoning"; summary: never[]; encrypted_content: string }; - -function toolResultText( - block: Extract, -): string { - const parts: string[] = []; - for (const c of block.content) { - if (c.type === "text") parts.push(c.text); - else parts.push(`[unsupported ${c.type} content omitted]`); - } - return parts.join(""); -} - -// Map one internal turn to Responses items. Text-only messages keep the string -// shape grok sends; messages with image blocks switch to Responses content parts -// so the model receives the actual pixels instead of only a text placeholder. -function toResponsesItems( - turn: ConversationTurn, - requestModel: string, - requestProvider: string, -): ResponsesInputItem[] { - const items: ResponsesInputItem[] = []; - const role = turn.role; - const parts: ResponsesInputContentPart[] = []; - let hasImage = false; - // See codex-responses-adapter.ts toResponsesItems for why an unreplayed - // reasoning item suppresses the function_call(s) it produced. - let suppressOrphanedCalls = false; - - const flushMessage = (): void => { - if (parts.length === 0) return; - items.push({ - type: "message", - role, - content: hasImage - ? [...parts] - : parts - .map((part) => (part.type === "input_text" ? part.text : "")) - .join(""), - }); - parts.length = 0; - hasImage = false; - suppressOrphanedCalls = false; - }; - - for (const block of turn.content) { - if (block.type === "text") { - parts.push({ type: "input_text", text: block.text }); - } else if (block.type === "image") { - if (block.source.kind === "base64") { - hasImage = true; - parts.push({ - type: "input_image", - image_url: `data:${block.source.mimeType};base64,${block.source.data}`, - }); - } else if (block.source.kind === "url") { - hasImage = true; - parts.push({ type: "input_image", image_url: block.source.url }); - } else { - parts.push({ - type: "input_text", - text: `[Unsupported image reference omitted: ${block.source.reference}]`, - }); - } - } else if (block.type === "tool_call") { - if (suppressOrphanedCalls) continue; - flushMessage(); - items.push({ - type: "function_call", - name: encodeToolName(block.name, RESPONSES_TOOL_NAME_LIMIT), - arguments: JSON.stringify(block.arguments ?? {}), - call_id: block.id, - }); - } else if (block.type === "tool_result") { - flushMessage(); - suppressOrphanedCalls = false; - items.push({ - type: "function_call_output", - call_id: block.callId, - output: toolResultText(block), - }); - } else if ( - block.type === "thinking" && - typeof block.signature === "string" && - block.signature.length > 0 - ) { - flushMessage(); - const encryptedContent = signatureForModel( - turn, - requestModel, - requestProvider, - block.signature, - ); - if (encryptedContent !== undefined) { - items.push({ - type: "reasoning", - summary: [], - encrypted_content: encryptedContent, - }); - suppressOrphanedCalls = false; - } else { - suppressOrphanedCalls = true; - } - } - } - flushMessage(); - return items; -} - -function toResponsesTools(options: InferenceOptions): unknown[] | undefined { - if (options.tools === undefined || options.tools.length === 0) - return undefined; - return options.tools.map((t) => ({ - type: "function", - name: encodeToolName(t.name, RESPONSES_TOOL_NAME_LIMIT), - description: t.description, - parameters: t.inputSchema, - })); -} - -function optionString( - options: InferenceOptions, - key: string, -): string | undefined { - const value = options.providerOptions?.[key]; - return typeof value === "string" && value.length > 0 ? value : undefined; -} - -// Keeps the LAST occurrence of each duplicate function_call / function_call_output -// call_id, not the first: a duplicate is most often a corrected retry, and -// discarding the retry in favor of the stale original silently replays the -// wrong tool result. Both item types are covered — a duplicated function_call -// is just as invalid on the wire as a duplicated output. -function dedupeToolItems(items: ResponsesInputItem[]): ResponsesInputItem[] { - const lastIndexForCall = new Map(); - items.forEach((item, i) => { - if (item.type === "function_call" || item.type === "function_call_output") { - lastIndexForCall.set(`${item.type}:${item.call_id}`, i); - } - }); - return items.filter((item, i) => { - if (item.type === "function_call" || item.type === "function_call_output") { - return lastIndexForCall.get(`${item.type}:${item.call_id}`) === i; - } - return true; - }); -} - -function buildRequest( - messages: ConversationTurn[], - model: string, - options: InferenceOptions, - requestProvider: string, -): BuiltRequest { - const conversation = dedupeToolItems( - messages.flatMap((turn) => toResponsesItems(turn, model, requestProvider)), - ); - const systemMessage: ResponsesInputItem | undefined = - options.systemPrompt !== undefined - ? { type: "message", role: "system", content: options.systemPrompt } - : undefined; - const input = - systemMessage !== undefined - ? [systemMessage, ...conversation] - : conversation; - const tools = toResponsesTools(options); - - const reasoning: { summary: "detailed"; effort?: string } = { - summary: "detailed", - }; - const effort = optionString(options, "reasoning_effort"); - if (effort !== undefined) reasoning.effort = effort; - - const body: Record = { - model, - input, - store: false, - stream: true, - include: ["reasoning.encrypted_content"], - // "detailed" streams denser summary deltas than "auto". Grok bills full - // thinking tokens but only returns summarized text; sparse auto summaries - // left the stall/activity clocks quiet for 60–120s mid-think. Effort is - // forwarded when the source set it — this adapter does not invent a default. - reasoning, - }; - if (tools !== undefined) { - body["tools"] = tools; - body["tool_choice"] = "auto"; - } - // With store:false this is the only cache-routing signal; keying it to the - // inference thread's session id keeps every request on the same cache shard. - const sessionId = optionString(options, GROK_SESSION_ID_OPTION); - if (sessionId !== undefined) body["prompt_cache_key"] = sessionId; - - const headers: Record = { - "content-type": "application/json", - accept: "text/event-stream", - authorization: BEARER_CREDENTIAL_SENTINEL, - "user-agent": XAI_USER_AGENT, - "x-grok-client-identifier": XAI_CLIENT_IDENTIFIER, - "x-grok-client-version": XAI_CLIENT_VERSION, - "x-grok-model-override": model, - }; - const userId = optionString(options, GROK_USER_ID_OPTION); - if (userId !== undefined) headers["x-grok-user-id"] = userId; - - return { url: XAI_RESPONSES_PATH, headers, body: JSON.stringify(body) }; -} - -export function createGrokResponsesAdapter( - source: LastCycleSource, -): ProviderAdapter { - // Re-created per request in buildRequest — see codex-responses-adapter.ts. - let indexer = createResponsesBlockIndexer(); - return { - buildRequest: (messages, model, options) => { - indexer = createResponsesBlockIndexer(); - return buildRequest(messages, model, options, source.provider); - }, - parseResponse: (sseData) => - parseResponse(sseData, indexer, source, GROK_RESPONSES_PROVIDER), - parseJSONResponse, - extractRetryAfterMs: extractResponsesRetryAfterMs, - }; -} diff --git a/src/provider/grok-responses-adapter.test.ts b/src/provider/grok-responses.test.ts similarity index 91% rename from src/provider/grok-responses-adapter.test.ts rename to src/provider/grok-responses.test.ts index 4744c69c1..fcbc7cb35 100644 --- a/src/provider/grok-responses-adapter.test.ts +++ b/src/provider/grok-responses.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; import type { ConversationTurn, LastCycleSource } from "@intx/types/runtime"; -import { createGrokResponsesAdapter } from "./grok-responses-adapter.js"; +import { createGrokResponsesAdapter } from "./grok-responses.js"; const source: LastCycleSource = { sourceId: "xai/test", @@ -199,15 +199,19 @@ describe("createGrokResponsesAdapter", () => { test("treats Responses completed, incomplete, and done events as stream-terminal", () => { const adapter = createGrokResponsesAdapter(source); - expect(typeof adapter.isStreamTerminal).toBe("function"); + const isStreamTerminal = adapter.isStreamTerminal; + expect(typeof isStreamTerminal).toBe("function"); + if (typeof isStreamTerminal !== "function") { + throw new Error("expected isStreamTerminal to be a function"); + } for (const type of ["response.completed", "response.incomplete", "response.done"]) { - expect(adapter.isStreamTerminal!(JSON.stringify({ type }))).toBe(true); + expect(isStreamTerminal(JSON.stringify({ type }))).toBe(true); } for (const type of ["response.output_text.delta", "response.created", "response.in_progress"]) { - expect(adapter.isStreamTerminal!(JSON.stringify({ type }))).toBe(false); + expect(isStreamTerminal(JSON.stringify({ type }))).toBe(false); } - expect(adapter.isStreamTerminal!("{not json")).toBe(false); - expect(adapter.isStreamTerminal!("null")).toBe(false); - expect(adapter.isStreamTerminal!('"just a string"')).toBe(false); + expect(isStreamTerminal("{not json")).toBe(false); + expect(isStreamTerminal("null")).toBe(false); + expect(isStreamTerminal('"just a string"')).toBe(false); }); }); diff --git a/src/provider/grok-responses.ts b/src/provider/grok-responses.ts new file mode 100644 index 000000000..e54f9e5c8 --- /dev/null +++ b/src/provider/grok-responses.ts @@ -0,0 +1,23 @@ +import { responsesAdapterFactory } from "@corbits/openai-responses"; +import { XAI_USER_ID_OPTION, xaiResponsesQuirks } from "@corbits/xai-provider"; +import type { AdapterFactory } from "@intx/inference"; + +export const GROK_RESPONSES_PROVIDER = "grok-responses"; + +export const GROK_USER_ID_OPTION = "grokUserId"; +export const GROK_SESSION_ID_OPTION = "grokSessionId"; + +// Spread xAI quirks and remap only host option keys. Config still writes +// grokUserId / grokSessionId / reasoning_effort; wrapping buildRequest to +// alias those would hide the mismatch instead of baking the host names. +export const createGrokResponsesAdapter: AdapterFactory = responsesAdapterFactory({ + ...xaiResponsesQuirks, + sessionIdOption: GROK_SESSION_ID_OPTION, + headers: { + ...xaiResponsesQuirks.headers, + fromOption: (xaiResponsesQuirks.headers?.fromOption ?? []).map((entry) => + entry.optionKey === XAI_USER_ID_OPTION ? { ...entry, optionKey: GROK_USER_ID_OPTION } : entry, + ), + }, + reasoning: { ...xaiResponsesQuirks.reasoning, effortOption: "reasoning_effort" }, +}); diff --git a/src/provider/inference-dependencies.ts b/src/provider/inference-dependencies.ts index 4cdf93fd2..b3f5634dd 100644 --- a/src/provider/inference-dependencies.ts +++ b/src/provider/inference-dependencies.ts @@ -7,7 +7,7 @@ import { loadAdapterRegistry } from "@intx/inference/providers"; import * as openaiCompatible from "./openai-compatible-adapter.js"; import * as opencodeGo from "./opencode-go-adapter.js"; import * as codexResponses from "./codex-responses-adapter.js"; -import * as grokResponses from "./grok-responses-adapter.js"; +import * as grokResponses from "./grok-responses.js"; import * as bifrostAdapter from "./bifrost-adapter.js"; import * as openaiResponses from "./openai-responses.js"; import * as opencodeGoAnthropic from "./opencode-go-anthropic-adapter.js"; @@ -15,7 +15,7 @@ import { CODEX_RESPONSES_PROVIDER, withCodexContentTypeRepair, } from "./codex-responses-adapter.js"; -import { GROK_RESPONSES_PROVIDER } from "./grok-responses-adapter.js"; +import { GROK_RESPONSES_PROVIDER } from "./grok-responses.js"; import { withReplaySanitizer } from "./replay-sanitizer.js"; import { OPENCODE_GO_PROVIDER_ID } from "../../packages/opencode-go/src/index.js"; import { BIFROST_PROVIDER } from "./bifrost-adapter.js"; @@ -44,7 +44,7 @@ const manifest: AdapterManifest = [ }, { provider: GROK_RESPONSES_PROVIDER, - specifier: "grok-responses-adapter", + specifier: "grok-responses", export: "createGrokResponsesAdapter", }, { @@ -68,7 +68,7 @@ const localModules: Record = { "openai-compatible-adapter": openaiCompatible, "opencode-go-adapter": opencodeGo, "codex-responses-adapter": codexResponses, - "grok-responses-adapter": grokResponses, + "grok-responses": grokResponses, "bifrost-adapter": bifrostAdapter, "openai-responses": openaiResponses, "opencode-go-anthropic-adapter": opencodeGoAnthropic, diff --git a/src/provider/replay-sanitizer.test.ts b/src/provider/replay-sanitizer.test.ts index 8de165666..0586fd7f5 100644 --- a/src/provider/replay-sanitizer.test.ts +++ b/src/provider/replay-sanitizer.test.ts @@ -8,7 +8,7 @@ import { createCodexResponsesAdapter, tagSignature, } from "./codex-responses-adapter.js"; -import { createGrokResponsesAdapter } from "./grok-responses-adapter.js"; +import { createGrokResponsesAdapter } from "./grok-responses.js"; import { createOpenAICompatibleAdapter } from "./openai-compatible-adapter.js"; import { sanitizeReplayTurns, diff --git a/src/subagent/provider-family.ts b/src/subagent/provider-family.ts index 40087af11..0dfe3e0d9 100644 --- a/src/subagent/provider-family.ts +++ b/src/subagent/provider-family.ts @@ -1,4 +1,4 @@ -import { GROK_RESPONSES_PROVIDER } from "../provider/grok-responses-adapter.js"; +import { GROK_RESPONSES_PROVIDER } from "../provider/grok-responses.js"; import { isXaiProviderName } from "../config/xai-providers.js"; /** diff --git a/tests/unit/codex-responses-adapter.test.ts b/tests/unit/codex-responses-adapter.test.ts index 2e648d33a..dccfb88f5 100644 --- a/tests/unit/codex-responses-adapter.test.ts +++ b/tests/unit/codex-responses-adapter.test.ts @@ -7,7 +7,7 @@ import { CODEX_SESSION_ID_OPTION, CODEX_RESPONSES_PROVIDER, } from "../../src/provider/codex-responses-adapter.js"; -import { GROK_RESPONSES_PROVIDER } from "../../src/provider/grok-responses-adapter.js"; +import { GROK_RESPONSES_PROVIDER } from "../../src/provider/grok-responses.js"; import { COMPACTED_PREFIX } from "../../src/session/compactor.js"; import { BEARER_CREDENTIAL_SENTINEL } from "@intx/inference"; import type { diff --git a/tests/unit/grok-responses-adapter.test.ts b/tests/unit/grok-responses-adapter.test.ts index be1fc765e..a3c6fef3c 100644 --- a/tests/unit/grok-responses-adapter.test.ts +++ b/tests/unit/grok-responses-adapter.test.ts @@ -3,7 +3,7 @@ import { createGrokResponsesAdapter, GROK_SESSION_ID_OPTION, GROK_USER_ID_OPTION, -} from "../../src/provider/grok-responses-adapter.js"; +} from "../../src/provider/grok-responses.js"; import { BEARER_CREDENTIAL_SENTINEL } from "@intx/inference"; import type { ConversationTurn, From 1ca081f9a4067761b9a8a737a4fc8a5632297f4d Mon Sep 17 00:00:00 2001 From: Sawyer Date: Thu, 10 Sep 2026 06:22:22 -0700 Subject: [PATCH 6/6] Format Responses adapter files with oxfmt --- src/provider/grok-responses.test.ts | 12 ++++++-- src/provider/grok-responses.ts | 28 +++++++++++-------- src/provider/openai-responses.ts | 12 ++++++-- src/provider/opencode-go-adapter.ts | 5 +++- src/provider/opencode-go-anthropic-adapter.ts | 5 +++- src/provider/opencode-session.ts | 5 +++- 6 files changed, 48 insertions(+), 19 deletions(-) diff --git a/src/provider/grok-responses.test.ts b/src/provider/grok-responses.test.ts index fcbc7cb35..9a2cf488e 100644 --- a/src/provider/grok-responses.test.ts +++ b/src/provider/grok-responses.test.ts @@ -204,10 +204,18 @@ describe("createGrokResponsesAdapter", () => { if (typeof isStreamTerminal !== "function") { throw new Error("expected isStreamTerminal to be a function"); } - for (const type of ["response.completed", "response.incomplete", "response.done"]) { + for (const type of [ + "response.completed", + "response.incomplete", + "response.done", + ]) { expect(isStreamTerminal(JSON.stringify({ type }))).toBe(true); } - for (const type of ["response.output_text.delta", "response.created", "response.in_progress"]) { + for (const type of [ + "response.output_text.delta", + "response.created", + "response.in_progress", + ]) { expect(isStreamTerminal(JSON.stringify({ type }))).toBe(false); } expect(isStreamTerminal("{not json")).toBe(false); diff --git a/src/provider/grok-responses.ts b/src/provider/grok-responses.ts index e54f9e5c8..0b51d35fa 100644 --- a/src/provider/grok-responses.ts +++ b/src/provider/grok-responses.ts @@ -10,14 +10,20 @@ export const GROK_SESSION_ID_OPTION = "grokSessionId"; // Spread xAI quirks and remap only host option keys. Config still writes // grokUserId / grokSessionId / reasoning_effort; wrapping buildRequest to // alias those would hide the mismatch instead of baking the host names. -export const createGrokResponsesAdapter: AdapterFactory = responsesAdapterFactory({ - ...xaiResponsesQuirks, - sessionIdOption: GROK_SESSION_ID_OPTION, - headers: { - ...xaiResponsesQuirks.headers, - fromOption: (xaiResponsesQuirks.headers?.fromOption ?? []).map((entry) => - entry.optionKey === XAI_USER_ID_OPTION ? { ...entry, optionKey: GROK_USER_ID_OPTION } : entry, - ), - }, - reasoning: { ...xaiResponsesQuirks.reasoning, effortOption: "reasoning_effort" }, -}); +export const createGrokResponsesAdapter: AdapterFactory = + responsesAdapterFactory({ + ...xaiResponsesQuirks, + sessionIdOption: GROK_SESSION_ID_OPTION, + headers: { + ...xaiResponsesQuirks.headers, + fromOption: (xaiResponsesQuirks.headers?.fromOption ?? []).map((entry) => + entry.optionKey === XAI_USER_ID_OPTION + ? { ...entry, optionKey: GROK_USER_ID_OPTION } + : entry, + ), + }, + reasoning: { + ...xaiResponsesQuirks.reasoning, + effortOption: "reasoning_effort", + }, + }); diff --git a/src/provider/openai-responses.ts b/src/provider/openai-responses.ts index 8dd65f6cb..e2df30168 100644 --- a/src/provider/openai-responses.ts +++ b/src/provider/openai-responses.ts @@ -1,4 +1,7 @@ -import { responsesAdapterFactory, type ResponsesQuirks } from "@corbits/openai-responses"; +import { + responsesAdapterFactory, + type ResponsesQuirks, +} from "@corbits/openai-responses"; import type { AdapterFactory } from "@intx/inference"; import { OPENCODE_SESSION_ID_OPTION } from "./opencode-session.js"; @@ -12,11 +15,14 @@ const hostQuirks: ResponsesQuirks = { path: "/responses", sessionIdOption: OPENAI_SESSION_ID_OPTION, headers: { - fromOption: [{ optionKey: OPENCODE_SESSION_ID_OPTION, header: "x-opencode-session" }], + fromOption: [ + { optionKey: OPENCODE_SESSION_ID_OPTION, header: "x-opencode-session" }, + ], }, systemPrompt: { role: "system", shape: "string" }, contentShape: "flat", reasoning: { summary: "auto" }, }; -export const createOpenAIResponsesAdapter: AdapterFactory = responsesAdapterFactory(hostQuirks); +export const createOpenAIResponsesAdapter: AdapterFactory = + responsesAdapterFactory(hostQuirks); diff --git a/src/provider/opencode-go-adapter.ts b/src/provider/opencode-go-adapter.ts index 1d072b776..c44f42fce 100644 --- a/src/provider/opencode-go-adapter.ts +++ b/src/provider/opencode-go-adapter.ts @@ -1,6 +1,9 @@ import type { BuiltRequest, ProviderAdapter } from "@intx/inference"; import { createOpenAICompatibleAdapter } from "./openai-compatible-adapter.js"; -import { OPENCODE_SESSION_ID_OPTION, optionString } from "./opencode-session.js"; +import { + OPENCODE_SESSION_ID_OPTION, + optionString, +} from "./opencode-session.js"; type AdapterSource = Parameters[0]; diff --git a/src/provider/opencode-go-anthropic-adapter.ts b/src/provider/opencode-go-anthropic-adapter.ts index 3c8090b50..f505e0956 100644 --- a/src/provider/opencode-go-anthropic-adapter.ts +++ b/src/provider/opencode-go-anthropic-adapter.ts @@ -1,6 +1,9 @@ import { type BuiltRequest, type ProviderAdapter } from "@intx/inference"; import { createAnthropicAdapter } from "@intx/inference/providers"; -import { OPENCODE_SESSION_ID_OPTION, optionString } from "./opencode-session.js"; +import { + OPENCODE_SESSION_ID_OPTION, + optionString, +} from "./opencode-session.js"; export const OPENCODE_GO_MESSAGES_PROVIDER = "opencode-go-messages"; diff --git a/src/provider/opencode-session.ts b/src/provider/opencode-session.ts index 48f1c06c4..e13f09bdc 100644 --- a/src/provider/opencode-session.ts +++ b/src/provider/opencode-session.ts @@ -2,7 +2,10 @@ import type { InferenceOptions } from "@intx/types/runtime"; export const OPENCODE_SESSION_ID_OPTION = "opencodeSessionId"; -export function optionString(options: InferenceOptions, key: string): string | undefined { +export function optionString( + options: InferenceOptions, + key: string, +): string | undefined { const value = options.providerOptions?.[key]; return typeof value === "string" && value.length > 0 ? value : undefined; }