fix(opencode): support native OpenAI OAuth fetch (#28571)
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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))),
|
||||
|
||||
Reference in New Issue
Block a user