chore(provider): mark internal transport purpose

This commit is contained in:
Aiden Cline
2026-05-26 15:06:08 -05:00
parent 891ad36ff4
commit bb6c1e073f
4 changed files with 63 additions and 4 deletions
+13 -4
View File
@@ -2,6 +2,7 @@
// https://github.com/vercel-labs/ai-sdk-openai-websocket/blob/main/packages/ai-sdk-openai-websocket-fetch/src/index.ts
import WebSocket from "ws"
import { ProviderTransport } from "@/provider/transport"
export interface CreateWebSocketFetchOptions {
/**
@@ -75,23 +76,31 @@ export function createWebSocketFetch(options?: CreateWebSocketFetchOptions) {
async function websocketFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
const url = input instanceof URL ? input.toString() : typeof input === "string" ? input : input.url
const internalHeaders = normalizeHeaders(init?.headers)
const httpInit = ProviderTransport.withoutInternalHeaders(init)
if (init?.method !== "POST" || !url.endsWith("/responses")) {
return globalThis.fetch(input, init)
return globalThis.fetch(input, httpInit)
}
let body: Record<string, unknown>
try {
body = JSON.parse(typeof init.body === "string" ? init.body : "")
} catch {
return globalThis.fetch(input, init)
return globalThis.fetch(input, httpInit)
}
// Temporary title-generation split: title requests share the conversation session ID today,
// so do not let them occupy or mutate the conversation WebSocket pool.
if (internalHeaders[ProviderTransport.INTERNAL_TRANSPORT_PURPOSE_HEADER] === ProviderTransport.PURPOSE.title) {
return globalThis.fetch(input, httpInit)
}
if (!body.stream) {
return globalThis.fetch(input, init)
return globalThis.fetch(input, httpInit)
}
const headers = normalizeHeaders(init.headers)
const headers = normalizeHeaders(httpInit?.headers)
delete headers["content-length"]
headers["openai-beta"] ??= "responses_websockets=2026-02-06"
const wsUrl = options?.url ?? url.replace(/^http/, "ws")
@@ -25,6 +25,7 @@ import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { isRecord } from "@/util/record"
import { optionalOmitUndefined } from "@opencode-ai/core/schema"
import * as ProviderTransform from "./transform"
import { ProviderTransport } from "./transport"
import { ModelID, ProviderID } from "./schema"
import { ModelStatus } from "./model-status"
import { RuntimeFlags } from "@/effect/runtime-flags"
@@ -1617,6 +1618,11 @@ export const layer = Layer.effect(
const combined = signals.length === 0 ? null : signals.length === 1 ? signals[0] : AbortSignal.any(signals)
if (combined) opts.signal = combined
// The WebSocket transport temporarily receives opencode-only routing metadata through
// headers because provider SDKs are still created below this layer. Never forward that
// metadata to upstream providers; replace this once llm.ts constructs SDK models directly.
if (opts.headers) opts.headers = ProviderTransport.stripInternalHeaders(opts.headers)
// Strip openai itemId metadata following what codex does
if (
(model.api.npm === "@ai-sdk/openai" || model.api.npm === "@ai-sdk/azure") &&
@@ -0,0 +1,39 @@
export const INTERNAL_TRANSPORT_PURPOSE_HEADER = "x-opencode-internal-transport-purpose"
export const PURPOSE = {
conversation: "conversation",
title: "title",
} as const
export type Purpose = (typeof PURPOSE)[keyof typeof PURPOSE]
export function purposeForAgent(agent: string): Purpose {
if (agent === "title") return PURPOSE.title
return PURPOSE.conversation
}
export function withoutInternalHeaders<T extends { headers?: HeadersInit }>(init: T | undefined): T | undefined {
if (!init?.headers) return init
return {
...init,
headers: stripInternalHeaders(init.headers),
}
}
export function stripInternalHeaders(headers: HeadersInit): HeadersInit {
if (headers instanceof Headers) {
const next = new Headers(headers)
next.delete(INTERNAL_TRANSPORT_PURPOSE_HEADER)
return next
}
if (Array.isArray(headers)) {
return headers.filter((item) => item[0].toLowerCase() !== INTERNAL_TRANSPORT_PURPOSE_HEADER)
}
return Object.fromEntries(
Object.entries(headers).filter(([key]) => key.toLowerCase() !== INTERNAL_TRANSPORT_PURPOSE_HEADER),
)
}
export * as ProviderTransport from "./transport"
@@ -5,6 +5,7 @@ import { Permission } from "@/permission"
import type { Agent } from "@/agent/agent"
import type { MessageV2 } from "../message-v2"
import type { Provider } from "@/provider/provider"
import { ProviderTransport } from "@/provider/transport"
import { ProviderTransform } from "@/provider/transform"
import { SystemPrompt } from "../system"
import { InstallationVersion } from "@opencode-ai/core/installation/version"
@@ -181,6 +182,10 @@ export const prepare = Effect.fn("LLMRequestPrep.prepare")(function* (input: Pre
}),
...input.model.headers,
...headers,
// Temporary fetch-layer hack for the OpenAI WebSocket pool. Title generation currently
// shares the conversation session ID, so this lets transport split it out until llm.ts
// can pass real transport context without smuggling state through request headers.
[ProviderTransport.INTERNAL_TRANSPORT_PURPOSE_HEADER]: ProviderTransport.purposeForAgent(input.agent.name),
},
}
})