chore(provider): mark internal transport purpose
This commit is contained in:
@@ -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),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user