fix(opencode): support native OpenAI OAuth fetch (#28571)

This commit is contained in:
Kit Langton
2026-05-20 23:29:25 -04:00
committed by GitHub
parent 2b28cc9024
commit facd207396
9 changed files with 418 additions and 161 deletions

View File

@@ -220,8 +220,6 @@ const live: Layer.Layer<
provider: item,
auth: info,
llmClient,
isOpenaiOauth: prepared.isOpenaiOauth,
system: prepared.system,
messages: prepared.messages,
tools: prepared.tools,
toolChoice: input.toolChoice,

View File

@@ -6,6 +6,7 @@ import { isRecord } from "@/util/record"
import { asSchema, type ModelMessage, type Tool } from "ai"
import { Effect } from "effect"
import * as Stream from "effect/Stream"
import { FetchHttpClient } from "effect/unstable/http"
import { tool as nativeTool, ToolFailure, type JsonSchema, type LLMEvent } from "@opencode-ai/llm"
import type { LLMClientShape } from "@opencode-ai/llm/route"
import { LLMNative } from "./native-request"
@@ -22,8 +23,6 @@ type StreamInput = {
readonly provider: Provider.Info
readonly auth: Auth.Info | undefined
readonly llmClient: LLMClientShape
readonly isOpenaiOauth: boolean
readonly system: string[]
readonly messages: ModelMessage[]
readonly tools: Record<string, Tool>
readonly toolChoice?: "auto" | "required" | "none"
@@ -37,13 +36,22 @@ type StreamInput = {
}
export function status(input: Pick<StreamInput, "model" | "provider" | "auth">): RuntimeStatus {
return statusWithFetch(input, providerFetch(input))
}
function statusWithFetch(
input: Pick<StreamInput, "model" | "provider" | "auth">,
fetch: typeof globalThis.fetch | undefined,
): RuntimeStatus {
const providerID = input.model.providerID
if (providerID !== "openai" && providerID !== "anthropic" && !providerID.startsWith("opencode"))
return { type: "unsupported", reason: "provider is not openai, opencode, or anthropic" }
const npm = input.model.api.npm
if (npm !== "@ai-sdk/openai" && npm !== "@ai-sdk/openai-compatible" && npm !== "@ai-sdk/anthropic")
return { type: "unsupported", reason: "provider package is not OpenAI, OpenAI-compatible, or Anthropic" }
if (input.auth?.type === "oauth") return { type: "unsupported", reason: "OAuth auth is not supported" }
if (input.auth?.type === "oauth" && !(input.provider.id === "openai" && fetch)) {
return { type: "unsupported", reason: "OAuth auth requires a provider fetch override" }
}
const apiKey = typeof input.provider.options.apiKey === "string" ? input.provider.options.apiKey : input.provider.key
if (!apiKey) return { type: "unsupported", reason: "API key is not configured" }
@@ -56,33 +64,42 @@ export function status(input: Pick<StreamInput, "model" | "provider" | "auth">):
}
export function stream(input: StreamInput): StreamResult {
const current = status(input)
const fetch = providerFetch(input)
const current = statusWithFetch(input, fetch)
if (current.type === "unsupported") return current
// Integration point with @opencode-ai/llm: native-request lowers session data
// into an LLMRequest, then LLMClient handles route selection and transport.
const stream = input.llmClient.stream({
request: LLMNative.request({
model: input.model,
apiKey: current.apiKey,
baseURL: current.baseURL,
messages: ProviderTransform.message(input.messages, input.model, input.providerOptions ?? {}),
toolChoice: input.toolChoice,
temperature: input.temperature,
topP: input.topP,
topK: input.topK,
maxOutputTokens: input.maxOutputTokens,
providerOptions: ProviderTransform.providerOptions(input.model, input.providerOptions ?? {}),
headers: { ...providerHeaders(input.provider.options.headers), ...input.headers },
}),
tools: nativeTools(input.tools, input),
})
return {
...current,
stream: input.llmClient.stream({
request: LLMNative.request({
model: input.model,
apiKey: current.apiKey,
baseURL: current.baseURL,
system: input.isOpenaiOauth ? input.system : [],
messages: ProviderTransform.message(input.messages, input.model, input.providerOptions ?? {}),
toolChoice: input.toolChoice,
temperature: input.temperature,
topP: input.topP,
topK: input.topK,
maxOutputTokens: input.maxOutputTokens,
providerOptions: ProviderTransform.providerOptions(input.model, input.providerOptions ?? {}),
headers: { ...providerHeaders(input.provider.options.headers), ...input.headers },
}),
tools: nativeTools(input.tools, input),
}),
stream: fetch ? stream.pipe(Stream.provideService(FetchHttpClient.Fetch, fetch)) : stream,
}
}
function providerFetch(input: Pick<StreamInput, "provider" | "auth">): typeof globalThis.fetch | undefined {
if (input.provider.id !== "openai" || input.auth?.type !== "oauth") return undefined
const value: unknown = input.provider.options.fetch
if (typeof value !== "function") return undefined
return value as typeof globalThis.fetch
}
function providerHeaders(value: unknown): Record<string, string> | undefined {
if (!isRecord(value)) return undefined
return Object.fromEntries(

View File

@@ -34,7 +34,6 @@ type PrepareInput = {
}
export type Prepared = {
readonly isOpenaiOauth: boolean
readonly system: string[]
readonly messages: ModelMessage[]
readonly tools: Record<string, Tool>
@@ -161,7 +160,6 @@ export const prepare = Effect.fn("LLMRequestPrep.prepare")(function* (input: Pre
: undefined
return {
isOpenaiOauth,
system,
messages,
tools: Object.fromEntries(Object.entries(tools).toSorted(([a], [b]) => a.localeCompare(b))),