feat: unwrap provider namespaces to flat exports + barrel (#22760)

This commit is contained in:
Kit Langton
2026-04-16 01:02:50 -04:00
committed by GitHub
parent c8af8f96ce
commit 6b20838981
21 changed files with 1448 additions and 1452 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ import { generateObject, streamObject, type ModelMessage } from "ai"
import { Instance } from "../project/instance" import { Instance } from "../project/instance"
import { Truncate } from "../tool" import { Truncate } from "../tool"
import { Auth } from "../auth" import { Auth } from "../auth"
import { ProviderTransform } from "../provider/transform" import { ProviderTransform } from "../provider"
import PROMPT_GENERATE from "./generate.txt" import PROMPT_GENERATE from "./generate.txt"
import PROMPT_COMPACTION from "./prompt/compaction.txt" import PROMPT_COMPACTION from "./prompt/compaction.txt"
+1 -1
View File
@@ -18,7 +18,7 @@ import type {
} from "@octokit/webhooks-types" } from "@octokit/webhooks-types"
import { UI } from "../ui" import { UI } from "../ui"
import { cmd } from "./cmd" import { cmd } from "./cmd"
import { ModelsDev } from "../../provider/models" import { ModelsDev } from "../../provider"
import { Instance } from "@/project/instance" import { Instance } from "@/project/instance"
import { bootstrap } from "../bootstrap" import { bootstrap } from "../bootstrap"
import { SessionShare } from "@/share" import { SessionShare } from "@/share"
+1 -1
View File
@@ -2,7 +2,7 @@ import type { Argv } from "yargs"
import { Instance } from "../../project/instance" import { Instance } from "../../project/instance"
import { Provider } from "../../provider" import { Provider } from "../../provider"
import { ProviderID } from "../../provider/schema" import { ProviderID } from "../../provider/schema"
import { ModelsDev } from "../../provider/models" import { ModelsDev } from "../../provider"
import { cmd } from "./cmd" import { cmd } from "./cmd"
import { UI } from "../ui" import { UI } from "../ui"
import { EOL } from "os" import { EOL } from "os"
+1 -1
View File
@@ -3,7 +3,7 @@ import { AppRuntime } from "../../effect/app-runtime"
import { cmd } from "./cmd" import { cmd } from "./cmd"
import * as prompts from "@clack/prompts" import * as prompts from "@clack/prompts"
import { UI } from "../ui" import { UI } from "../ui"
import { ModelsDev } from "../../provider/models" import { ModelsDev } from "../../provider"
import { map, pipe, sortBy, values } from "remeda" import { map, pipe, sortBy, values } from "remeda"
import path from "path" import path from "path"
import os from "os" import os from "os"
+1 -1
View File
@@ -16,7 +16,7 @@ import { Storage } from "@/storage"
import { Snapshot } from "@/snapshot" import { Snapshot } from "@/snapshot"
import { Plugin } from "@/plugin" import { Plugin } from "@/plugin"
import { Provider } from "@/provider" import { Provider } from "@/provider"
import { ProviderAuth } from "@/provider/auth" import { ProviderAuth } from "@/provider"
import { Agent } from "@/agent/agent" import { Agent } from "@/agent/agent"
import { Skill } from "@/skill" import { Skill } from "@/skill"
import { Discovery } from "@/skill/discovery" import { Discovery } from "@/skill/discovery"
+190 -192
View File
@@ -9,219 +9,217 @@ import { ProviderID } from "./schema"
import { Array as Arr, Effect, Layer, Record, Result, Context, Schema } from "effect" import { Array as Arr, Effect, Layer, Record, Result, Context, Schema } from "effect"
import z from "zod" import z from "zod"
export namespace ProviderAuth { const When = Schema.Struct({
const When = Schema.Struct({ key: Schema.String,
key: Schema.String, op: Schema.Literals(["eq", "neq"]),
op: Schema.Literals(["eq", "neq"]), value: Schema.String,
value: Schema.String, })
})
const TextPrompt = Schema.Struct({ const TextPrompt = Schema.Struct({
type: Schema.Literal("text"), type: Schema.Literal("text"),
key: Schema.String, key: Schema.String,
message: Schema.String, message: Schema.String,
placeholder: Schema.optional(Schema.String), placeholder: Schema.optional(Schema.String),
when: Schema.optional(When), when: Schema.optional(When),
}) })
const SelectOption = Schema.Struct({ const SelectOption = Schema.Struct({
label: Schema.String, label: Schema.String,
value: Schema.String, value: Schema.String,
hint: Schema.optional(Schema.String), hint: Schema.optional(Schema.String),
}) })
const SelectPrompt = Schema.Struct({ const SelectPrompt = Schema.Struct({
type: Schema.Literal("select"), type: Schema.Literal("select"),
key: Schema.String, key: Schema.String,
message: Schema.String, message: Schema.String,
options: Schema.Array(SelectOption), options: Schema.Array(SelectOption),
when: Schema.optional(When), when: Schema.optional(When),
}) })
const Prompt = Schema.Union([TextPrompt, SelectPrompt]) const Prompt = Schema.Union([TextPrompt, SelectPrompt])
export class Method extends Schema.Class<Method>("ProviderAuthMethod")({ export class Method extends Schema.Class<Method>("ProviderAuthMethod")({
type: Schema.Literals(["oauth", "api"]), type: Schema.Literals(["oauth", "api"]),
label: Schema.String, label: Schema.String,
prompts: Schema.optional(Schema.Array(Prompt)), prompts: Schema.optional(Schema.Array(Prompt)),
}) { }) {
static readonly zod = zod(this) static readonly zod = zod(this)
} }
export const Methods = Schema.Record(Schema.String, Schema.Array(Method)).pipe(withStatics((s) => ({ zod: zod(s) }))) export const Methods = Schema.Record(Schema.String, Schema.Array(Method)).pipe(withStatics((s) => ({ zod: zod(s) })))
export type Methods = typeof Methods.Type export type Methods = typeof Methods.Type
export class Authorization extends Schema.Class<Authorization>("ProviderAuthAuthorization")({ export class Authorization extends Schema.Class<Authorization>("ProviderAuthAuthorization")({
url: Schema.String, url: Schema.String,
method: Schema.Literals(["auto", "code"]), method: Schema.Literals(["auto", "code"]),
instructions: Schema.String, instructions: Schema.String,
}) { }) {
static readonly zod = zod(this) static readonly zod = zod(this)
} }
export const OauthMissing = NamedError.create("ProviderAuthOauthMissing", z.object({ providerID: ProviderID.zod })) export const OauthMissing = NamedError.create("ProviderAuthOauthMissing", z.object({ providerID: ProviderID.zod }))
export const OauthCodeMissing = NamedError.create( export const OauthCodeMissing = NamedError.create(
"ProviderAuthOauthCodeMissing", "ProviderAuthOauthCodeMissing",
z.object({ providerID: ProviderID.zod }), z.object({ providerID: ProviderID.zod }),
) )
export const OauthCallbackFailed = NamedError.create("ProviderAuthOauthCallbackFailed", z.object({})) export const OauthCallbackFailed = NamedError.create("ProviderAuthOauthCallbackFailed", z.object({}))
export const ValidationFailed = NamedError.create( export const ValidationFailed = NamedError.create(
"ProviderAuthValidationFailed", "ProviderAuthValidationFailed",
z.object({ z.object({
field: z.string(), field: z.string(),
message: z.string(), message: z.string(),
}), }),
) )
export type Error = export type Error =
| Auth.AuthError | Auth.AuthError
| InstanceType<typeof OauthMissing> | InstanceType<typeof OauthMissing>
| InstanceType<typeof OauthCodeMissing> | InstanceType<typeof OauthCodeMissing>
| InstanceType<typeof OauthCallbackFailed> | InstanceType<typeof OauthCallbackFailed>
| InstanceType<typeof ValidationFailed> | InstanceType<typeof ValidationFailed>
type Hook = NonNullable<Hooks["auth"]> type Hook = NonNullable<Hooks["auth"]>
export interface Interface { export interface Interface {
readonly methods: () => Effect.Effect<Methods> readonly methods: () => Effect.Effect<Methods>
readonly authorize: (input: { readonly authorize: (input: {
providerID: ProviderID
method: number
inputs?: Record<string, string>
}) => Effect.Effect<Authorization | undefined, Error>
readonly callback: (input: { providerID: ProviderID; method: number; code?: string }) => Effect.Effect<void, Error>
}
interface State {
hooks: Record<ProviderID, Hook>
pending: Map<ProviderID, AuthOAuthResult>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/ProviderAuth") {}
export const layer: Layer.Layer<Service, never, Auth.Service | Plugin.Service> = Layer.effect(
Service,
Effect.gen(function* () {
const auth = yield* Auth.Service
const plugin = yield* Plugin.Service
const state = yield* InstanceState.make<State>(
Effect.fn("ProviderAuth.state")(function* () {
const plugins = yield* plugin.list()
return {
hooks: Record.fromEntries(
Arr.filterMap(plugins, (x) =>
x.auth?.provider !== undefined
? Result.succeed([ProviderID.make(x.auth.provider), x.auth] as const)
: Result.failVoid,
),
),
pending: new Map<ProviderID, AuthOAuthResult>(),
}
}),
)
const decode = Schema.decodeUnknownSync(Methods)
const methods = Effect.fn("ProviderAuth.methods")(function* () {
const hooks = (yield* InstanceState.get(state)).hooks
return decode(
Record.map(hooks, (item) =>
item.methods.map((method) => ({
type: method.type,
label: method.label,
prompts: method.prompts?.map((prompt) => {
if (prompt.type === "select") {
return {
type: "select" as const,
key: prompt.key,
message: prompt.message,
options: prompt.options,
when: prompt.when,
}
}
return {
type: "text" as const,
key: prompt.key,
message: prompt.message,
placeholder: prompt.placeholder,
when: prompt.when,
}
}),
})),
),
)
})
const authorize = Effect.fn("ProviderAuth.authorize")(function* (input: {
providerID: ProviderID providerID: ProviderID
method: number method: number
inputs?: Record<string, string> inputs?: Record<string, string>
}) => Effect.Effect<Authorization | undefined, Error> }) {
readonly callback: (input: { providerID: ProviderID; method: number; code?: string }) => Effect.Effect<void, Error> const { hooks, pending } = yield* InstanceState.get(state)
} const method = hooks[input.providerID].methods[input.method]
if (method.type !== "oauth") return
interface State { if (method.prompts && input.inputs) {
hooks: Record<ProviderID, Hook> for (const prompt of method.prompts) {
pending: Map<ProviderID, AuthOAuthResult> if (prompt.type === "text" && prompt.validate && input.inputs[prompt.key] !== undefined) {
} const error = prompt.validate(input.inputs[prompt.key])
if (error) return yield* Effect.fail(new ValidationFailed({ field: prompt.key, message: error }))
export class Service extends Context.Service<Service, Interface>()("@opencode/ProviderAuth") {}
export const layer: Layer.Layer<Service, never, Auth.Service | Plugin.Service> = Layer.effect(
Service,
Effect.gen(function* () {
const auth = yield* Auth.Service
const plugin = yield* Plugin.Service
const state = yield* InstanceState.make<State>(
Effect.fn("ProviderAuth.state")(function* () {
const plugins = yield* plugin.list()
return {
hooks: Record.fromEntries(
Arr.filterMap(plugins, (x) =>
x.auth?.provider !== undefined
? Result.succeed([ProviderID.make(x.auth.provider), x.auth] as const)
: Result.failVoid,
),
),
pending: new Map<ProviderID, AuthOAuthResult>(),
} }
}), }
}
const result = yield* Effect.promise(() => method.authorize(input.inputs))
pending.set(input.providerID, result)
return {
url: result.url,
method: result.method,
instructions: result.instructions,
}
})
const callback = Effect.fn("ProviderAuth.callback")(function* (input: {
providerID: ProviderID
method: number
code?: string
}) {
const pending = (yield* InstanceState.get(state)).pending
const match = pending.get(input.providerID)
if (!match) return yield* Effect.fail(new OauthMissing({ providerID: input.providerID }))
if (match.method === "code" && !input.code) {
return yield* Effect.fail(new OauthCodeMissing({ providerID: input.providerID }))
}
const result = yield* Effect.promise(() =>
match.method === "code" ? match.callback(input.code!) : match.callback(),
) )
if (!result || result.type !== "success") return yield* Effect.fail(new OauthCallbackFailed({}))
const decode = Schema.decodeUnknownSync(Methods) if ("key" in result) {
const methods = Effect.fn("ProviderAuth.methods")(function* () { yield* auth.set(input.providerID, {
const hooks = (yield* InstanceState.get(state)).hooks type: "api",
return decode( key: result.key,
Record.map(hooks, (item) => })
item.methods.map((method) => ({ }
type: method.type,
label: method.label,
prompts: method.prompts?.map((prompt) => {
if (prompt.type === "select") {
return {
type: "select" as const,
key: prompt.key,
message: prompt.message,
options: prompt.options,
when: prompt.when,
}
}
return {
type: "text" as const,
key: prompt.key,
message: prompt.message,
placeholder: prompt.placeholder,
when: prompt.when,
}
}),
})),
),
)
})
const authorize = Effect.fn("ProviderAuth.authorize")(function* (input: { if ("refresh" in result) {
providerID: ProviderID const { type: _, provider: __, refresh, access, expires, ...extra } = result
method: number yield* auth.set(input.providerID, {
inputs?: Record<string, string> type: "oauth",
}) { access,
const { hooks, pending } = yield* InstanceState.get(state) refresh,
const method = hooks[input.providerID].methods[input.method] expires,
if (method.type !== "oauth") return ...extra,
})
}
})
if (method.prompts && input.inputs) { return Service.of({ methods, authorize, callback })
for (const prompt of method.prompts) { }),
if (prompt.type === "text" && prompt.validate && input.inputs[prompt.key] !== undefined) { )
const error = prompt.validate(input.inputs[prompt.key])
if (error) return yield* Effect.fail(new ValidationFailed({ field: prompt.key, message: error }))
}
}
}
const result = yield* Effect.promise(() => method.authorize(input.inputs)) export const defaultLayer = Layer.suspend(() =>
pending.set(input.providerID, result) layer.pipe(Layer.provide(Auth.defaultLayer), Layer.provide(Plugin.defaultLayer)),
return { )
url: result.url,
method: result.method,
instructions: result.instructions,
}
})
const callback = Effect.fn("ProviderAuth.callback")(function* (input: {
providerID: ProviderID
method: number
code?: string
}) {
const pending = (yield* InstanceState.get(state)).pending
const match = pending.get(input.providerID)
if (!match) return yield* Effect.fail(new OauthMissing({ providerID: input.providerID }))
if (match.method === "code" && !input.code) {
return yield* Effect.fail(new OauthCodeMissing({ providerID: input.providerID }))
}
const result = yield* Effect.promise(() =>
match.method === "code" ? match.callback(input.code!) : match.callback(),
)
if (!result || result.type !== "success") return yield* Effect.fail(new OauthCallbackFailed({}))
if ("key" in result) {
yield* auth.set(input.providerID, {
type: "api",
key: result.key,
})
}
if ("refresh" in result) {
const { type: _, provider: __, refresh, access, expires, ...extra } = result
yield* auth.set(input.providerID, {
type: "oauth",
access,
refresh,
expires,
...extra,
})
}
})
return Service.of({ methods, authorize, callback })
}),
)
export const defaultLayer = Layer.suspend(() =>
layer.pipe(Layer.provide(Auth.defaultLayer), Layer.provide(Plugin.defaultLayer)),
)
}
+169 -171
View File
@@ -3,195 +3,193 @@ import { STATUS_CODES } from "http"
import { iife } from "@/util/iife" import { iife } from "@/util/iife"
import type { ProviderID } from "./schema" import type { ProviderID } from "./schema"
export namespace ProviderError { // Adapted from overflow detection patterns in:
// Adapted from overflow detection patterns in: // https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/utils/overflow.ts
// https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/utils/overflow.ts const OVERFLOW_PATTERNS = [
const OVERFLOW_PATTERNS = [ /prompt is too long/i, // Anthropic
/prompt is too long/i, // Anthropic /input is too long for requested model/i, // Amazon Bedrock
/input is too long for requested model/i, // Amazon Bedrock /exceeds the context window/i, // OpenAI (Completions + Responses API message text)
/exceeds the context window/i, // OpenAI (Completions + Responses API message text) /input token count.*exceeds the maximum/i, // Google (Gemini)
/input token count.*exceeds the maximum/i, // Google (Gemini) /maximum prompt length is \d+/i, // xAI (Grok)
/maximum prompt length is \d+/i, // xAI (Grok) /reduce the length of the messages/i, // Groq
/reduce the length of the messages/i, // Groq /maximum context length is \d+ tokens/i, // OpenRouter, DeepSeek, vLLM
/maximum context length is \d+ tokens/i, // OpenRouter, DeepSeek, vLLM /exceeds the limit of \d+/i, // GitHub Copilot
/exceeds the limit of \d+/i, // GitHub Copilot /exceeds the available context size/i, // llama.cpp server
/exceeds the available context size/i, // llama.cpp server /greater than the context length/i, // LM Studio
/greater than the context length/i, // LM Studio /context window exceeds limit/i, // MiniMax
/context window exceeds limit/i, // MiniMax /exceeded model token limit/i, // Kimi For Coding, Moonshot
/exceeded model token limit/i, // Kimi For Coding, Moonshot /context[_ ]length[_ ]exceeded/i, // Generic fallback
/context[_ ]length[_ ]exceeded/i, // Generic fallback /request entity too large/i, // HTTP 413
/request entity too large/i, // HTTP 413 /context length is only \d+ tokens/i, // vLLM
/context length is only \d+ tokens/i, // vLLM /input length.*exceeds.*context length/i, // vLLM
/input length.*exceeds.*context length/i, // vLLM /prompt too long; exceeded (?:max )?context length/i, // Ollama explicit overflow error
/prompt too long; exceeded (?:max )?context length/i, // Ollama explicit overflow error /too large for model with \d+ maximum context length/i, // Mistral
/too large for model with \d+ maximum context length/i, // Mistral /model_context_window_exceeded/i, // z.ai non-standard finish_reason surfaced as error text
/model_context_window_exceeded/i, // z.ai non-standard finish_reason surfaced as error text ]
]
function isOpenAiErrorRetryable(e: APICallError) { function isOpenAiErrorRetryable(e: APICallError) {
const status = e.statusCode const status = e.statusCode
if (!status) return e.isRetryable if (!status) return e.isRetryable
// openai sometimes returns 404 for models that are actually available // openai sometimes returns 404 for models that are actually available
return status === 404 || e.isRetryable return status === 404 || e.isRetryable
} }
// Providers not reliably handled in this function: // Providers not reliably handled in this function:
// - z.ai: can accept overflow silently (needs token-count/context-window checks) // - z.ai: can accept overflow silently (needs token-count/context-window checks)
function isOverflow(message: string) { function isOverflow(message: string) {
if (OVERFLOW_PATTERNS.some((p) => p.test(message))) return true if (OVERFLOW_PATTERNS.some((p) => p.test(message))) return true
// Providers/status patterns handled outside of regex list: // Providers/status patterns handled outside of regex list:
// - Cerebras: often returns "400 (no body)" / "413 (no body)" // - Cerebras: often returns "400 (no body)" / "413 (no body)"
// - Mistral: often returns "400 (no body)" / "413 (no body)" // - Mistral: often returns "400 (no body)" / "413 (no body)"
return /^4(00|13)\s*(status code)?\s*\(no body\)/i.test(message) return /^4(00|13)\s*(status code)?\s*\(no body\)/i.test(message)
} }
function message(providerID: ProviderID, e: APICallError) { function message(providerID: ProviderID, e: APICallError) {
return iife(() => { return iife(() => {
const msg = e.message const msg = e.message
if (msg === "") { if (msg === "") {
if (e.responseBody) return e.responseBody if (e.responseBody) return e.responseBody
if (e.statusCode) { if (e.statusCode) {
const err = STATUS_CODES[e.statusCode] const err = STATUS_CODES[e.statusCode]
if (err) return err if (err) return err
}
return "Unknown error"
}
if (!e.responseBody || (e.statusCode && msg !== STATUS_CODES[e.statusCode])) {
return msg
}
try {
const body = JSON.parse(e.responseBody)
// try to extract common error message fields
const errMsg = body.message || body.error || body.error?.message
if (errMsg && typeof errMsg === "string") {
return `${msg}: ${errMsg}`
}
} catch {}
// If responseBody is HTML (e.g. from a gateway or proxy error page),
// provide a human-readable message instead of dumping raw markup
if (/^\s*<!doctype|^\s*<html/i.test(e.responseBody)) {
if (e.statusCode === 401) {
return "Unauthorized: request was blocked by a gateway or proxy. Your authentication token may be missing or expired — try running `opencode auth login <your provider URL>` to re-authenticate."
}
if (e.statusCode === 403) {
return "Forbidden: request was blocked by a gateway or proxy. You may not have permission to access this resource — check your account and provider settings."
}
return msg
}
return `${msg}: ${e.responseBody}`
}).trim()
}
function json(input: unknown) {
if (typeof input === "string") {
try {
const result = JSON.parse(input)
if (result && typeof result === "object") return result
return undefined
} catch {
return undefined
} }
return "Unknown error"
} }
if (typeof input === "object" && input !== null) {
return input if (!e.responseBody || (e.statusCode && msg !== STATUS_CODES[e.statusCode])) {
return msg
} }
return undefined
}
export type ParsedStreamError = try {
| { const body = JSON.parse(e.responseBody)
type: "context_overflow" // try to extract common error message fields
message: string const errMsg = body.message || body.error || body.error?.message
responseBody: string if (errMsg && typeof errMsg === "string") {
return `${msg}: ${errMsg}`
} }
| { } catch {}
type: "api_error"
message: string // If responseBody is HTML (e.g. from a gateway or proxy error page),
isRetryable: false // provide a human-readable message instead of dumping raw markup
responseBody: string if (/^\s*<!doctype|^\s*<html/i.test(e.responseBody)) {
if (e.statusCode === 401) {
return "Unauthorized: request was blocked by a gateway or proxy. Your authentication token may be missing or expired — try running `opencode auth login <your provider URL>` to re-authenticate."
} }
if (e.statusCode === 403) {
return "Forbidden: request was blocked by a gateway or proxy. You may not have permission to access this resource — check your account and provider settings."
}
return msg
}
export function parseStreamError(input: unknown): ParsedStreamError | undefined { return `${msg}: ${e.responseBody}`
const body = json(input) }).trim()
if (!body) return }
const responseBody = JSON.stringify(body) function json(input: unknown) {
if (body.type !== "error") return if (typeof input === "string") {
try {
switch (body?.error?.code) { const result = JSON.parse(input)
case "context_length_exceeded": if (result && typeof result === "object") return result
return { return undefined
type: "context_overflow", } catch {
message: "Input exceeds context window of this model", return undefined
responseBody,
}
case "insufficient_quota":
return {
type: "api_error",
message: "Quota exceeded. Check your plan and billing details.",
isRetryable: false,
responseBody,
}
case "usage_not_included":
return {
type: "api_error",
message: "To use Codex with your ChatGPT plan, upgrade to Plus: https://chatgpt.com/explore/plus.",
isRetryable: false,
responseBody,
}
case "invalid_prompt":
return {
type: "api_error",
message: typeof body?.error?.message === "string" ? body?.error?.message : "Invalid prompt.",
isRetryable: false,
responseBody,
}
} }
} }
if (typeof input === "object" && input !== null) {
return input
}
return undefined
}
export type ParsedAPICallError = export type ParsedStreamError =
| { | {
type: "context_overflow" type: "context_overflow"
message: string message: string
responseBody?: string responseBody: string
} }
| { | {
type: "api_error" type: "api_error"
message: string message: string
statusCode?: number isRetryable: false
isRetryable: boolean responseBody: string
responseHeaders?: Record<string, string> }
responseBody?: string
metadata?: Record<string, string>
}
export function parseAPICallError(input: { providerID: ProviderID; error: APICallError }): ParsedAPICallError { export function parseStreamError(input: unknown): ParsedStreamError | undefined {
const m = message(input.providerID, input.error) const body = json(input)
const body = json(input.error.responseBody) if (!body) return
if (isOverflow(m) || input.error.statusCode === 413 || body?.error?.code === "context_length_exceeded") {
const responseBody = JSON.stringify(body)
if (body.type !== "error") return
switch (body?.error?.code) {
case "context_length_exceeded":
return { return {
type: "context_overflow", type: "context_overflow",
message: m, message: "Input exceeds context window of this model",
responseBody: input.error.responseBody, responseBody,
}
case "insufficient_quota":
return {
type: "api_error",
message: "Quota exceeded. Check your plan and billing details.",
isRetryable: false,
responseBody,
}
case "usage_not_included":
return {
type: "api_error",
message: "To use Codex with your ChatGPT plan, upgrade to Plus: https://chatgpt.com/explore/plus.",
isRetryable: false,
responseBody,
}
case "invalid_prompt":
return {
type: "api_error",
message: typeof body?.error?.message === "string" ? body?.error?.message : "Invalid prompt.",
isRetryable: false,
responseBody,
} }
} }
}
const metadata = input.error.url ? { url: input.error.url } : undefined
return { export type ParsedAPICallError =
type: "api_error", | {
message: m, type: "context_overflow"
statusCode: input.error.statusCode, message: string
isRetryable: input.providerID.startsWith("openai") responseBody?: string
? isOpenAiErrorRetryable(input.error) }
: input.error.isRetryable, | {
responseHeaders: input.error.responseHeaders, type: "api_error"
responseBody: input.error.responseBody, message: string
metadata, statusCode?: number
} isRetryable: boolean
responseHeaders?: Record<string, string>
responseBody?: string
metadata?: Record<string, string>
}
export function parseAPICallError(input: { providerID: ProviderID; error: APICallError }): ParsedAPICallError {
const m = message(input.providerID, input.error)
const body = json(input.error.responseBody)
if (isOverflow(m) || input.error.statusCode === 413 || body?.error?.code === "context_length_exceeded") {
return {
type: "context_overflow",
message: m,
responseBody: input.error.responseBody,
}
}
const metadata = input.error.url ? { url: input.error.url } : undefined
return {
type: "api_error",
message: m,
statusCode: input.error.statusCode,
isRetryable: input.providerID.startsWith("openai")
? isOpenAiErrorRetryable(input.error)
: input.error.isRetryable,
responseHeaders: input.error.responseHeaders,
responseBody: input.error.responseBody,
metadata,
} }
} }
+4
View File
@@ -1 +1,5 @@
export * as Provider from "./provider" export * as Provider from "./provider"
export * as ProviderAuth from "./auth"
export * as ProviderError from "./error"
export * as ModelsDev from "./models"
export * as ProviderTransform from "./transform"
+146 -148
View File
@@ -13,169 +13,167 @@ import { Hash } from "@opencode-ai/shared/util/hash"
// Falls back to undefined in dev mode when snapshot doesn't exist // Falls back to undefined in dev mode when snapshot doesn't exist
/* @ts-ignore */ /* @ts-ignore */
export namespace ModelsDev { const log = Log.create({ service: "models.dev" })
const log = Log.create({ service: "models.dev" }) const source = url()
const source = url() const filepath = path.join(
const filepath = path.join( Global.Path.cache,
Global.Path.cache, source === "https://models.dev" ? "models.json" : `models-${Hash.fast(source)}.json`,
source === "https://models.dev" ? "models.json" : `models-${Hash.fast(source)}.json`, )
) const ttl = 5 * 60 * 1000
const ttl = 5 * 60 * 1000
type JsonValue = string | number | boolean | null | { [key: string]: JsonValue } | JsonValue[] type JsonValue = string | number | boolean | null | { [key: string]: JsonValue } | JsonValue[]
const JsonValue: z.ZodType<JsonValue> = z.lazy(() => const JsonValue: z.ZodType<JsonValue> = z.lazy(() =>
z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(JsonValue), z.record(z.string(), JsonValue)]), z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(JsonValue), z.record(z.string(), JsonValue)]),
) )
const Cost = z.object({ const Cost = z.object({
input: z.number(), input: z.number(),
output: z.number(), output: z.number(),
cache_read: z.number().optional(), cache_read: z.number().optional(),
cache_write: z.number().optional(), cache_write: z.number().optional(),
context_over_200k: z context_over_200k: z
.object({ .object({
input: z.number(), input: z.number(),
output: z.number(),
cache_read: z.number().optional(),
cache_write: z.number().optional(),
})
.optional(),
})
export const Model = z.object({
id: z.string(),
name: z.string(),
family: z.string().optional(),
release_date: z.string(),
attachment: z.boolean(),
reasoning: z.boolean(),
temperature: z.boolean(),
tool_call: z.boolean(),
interleaved: z
.union([
z.literal(true),
z
.object({
field: z.enum(["reasoning_content", "reasoning_details"]),
})
.strict(),
])
.optional(),
cost: Cost.optional(),
limit: z.object({
context: z.number(),
input: z.number().optional(),
output: z.number(), output: z.number(),
}), cache_read: z.number().optional(),
modalities: z cache_write: z.number().optional(),
.object({
input: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
output: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
})
.optional(),
experimental: z
.object({
modes: z
.record(
z.string(),
z.object({
cost: Cost.optional(),
provider: z
.object({
body: z.record(z.string(), JsonValue).optional(),
headers: z.record(z.string(), z.string()).optional(),
})
.optional(),
}),
)
.optional(),
})
.optional(),
status: z.enum(["alpha", "beta", "deprecated"]).optional(),
provider: z.object({ npm: z.string().optional(), api: z.string().optional() }).optional(),
})
export type Model = z.infer<typeof Model>
export const Provider = z.object({
api: z.string().optional(),
name: z.string(),
env: z.array(z.string()),
id: z.string(),
npm: z.string().optional(),
models: z.record(z.string(), Model),
})
export type Provider = z.infer<typeof Provider>
function url() {
return Flag.OPENCODE_MODELS_URL || "https://models.dev"
}
function fresh() {
return Date.now() - Number(Filesystem.stat(filepath)?.mtimeMs ?? 0) < ttl
}
function skip(force: boolean) {
return !force && fresh()
}
const fetchApi = async () => {
const result = await fetch(`${url()}/api.json`, {
headers: { "User-Agent": Installation.USER_AGENT },
signal: AbortSignal.timeout(10000),
}) })
return { ok: result.ok, text: await result.text() } .optional(),
} })
export const Data = lazy(async () => { export const Model = z.object({
id: z.string(),
name: z.string(),
family: z.string().optional(),
release_date: z.string(),
attachment: z.boolean(),
reasoning: z.boolean(),
temperature: z.boolean(),
tool_call: z.boolean(),
interleaved: z
.union([
z.literal(true),
z
.object({
field: z.enum(["reasoning_content", "reasoning_details"]),
})
.strict(),
])
.optional(),
cost: Cost.optional(),
limit: z.object({
context: z.number(),
input: z.number().optional(),
output: z.number(),
}),
modalities: z
.object({
input: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
output: z.array(z.enum(["text", "audio", "image", "video", "pdf"])),
})
.optional(),
experimental: z
.object({
modes: z
.record(
z.string(),
z.object({
cost: Cost.optional(),
provider: z
.object({
body: z.record(z.string(), JsonValue).optional(),
headers: z.record(z.string(), z.string()).optional(),
})
.optional(),
}),
)
.optional(),
})
.optional(),
status: z.enum(["alpha", "beta", "deprecated"]).optional(),
provider: z.object({ npm: z.string().optional(), api: z.string().optional() }).optional(),
})
export type Model = z.infer<typeof Model>
export const Provider = z.object({
api: z.string().optional(),
name: z.string(),
env: z.array(z.string()),
id: z.string(),
npm: z.string().optional(),
models: z.record(z.string(), Model),
})
export type Provider = z.infer<typeof Provider>
function url() {
return Flag.OPENCODE_MODELS_URL || "https://models.dev"
}
function fresh() {
return Date.now() - Number(Filesystem.stat(filepath)?.mtimeMs ?? 0) < ttl
}
function skip(force: boolean) {
return !force && fresh()
}
const fetchApi = async () => {
const result = await fetch(`${url()}/api.json`, {
headers: { "User-Agent": Installation.USER_AGENT },
signal: AbortSignal.timeout(10000),
})
return { ok: result.ok, text: await result.text() }
}
export const Data = lazy(async () => {
const result = await Filesystem.readJson(Flag.OPENCODE_MODELS_PATH ?? filepath).catch(() => {})
if (result) return result
// @ts-ignore
const snapshot = await import("./models-snapshot.js")
.then((m) => m.snapshot as Record<string, unknown>)
.catch(() => undefined)
if (snapshot) return snapshot
if (Flag.OPENCODE_DISABLE_MODELS_FETCH) return {}
return Flock.withLock(`models-dev:${filepath}`, async () => {
const result = await Filesystem.readJson(Flag.OPENCODE_MODELS_PATH ?? filepath).catch(() => {}) const result = await Filesystem.readJson(Flag.OPENCODE_MODELS_PATH ?? filepath).catch(() => {})
if (result) return result if (result) return result
// @ts-ignore const result2 = await fetchApi()
const snapshot = await import("./models-snapshot.js") if (result2.ok) {
.then((m) => m.snapshot as Record<string, unknown>) await Filesystem.write(filepath, result2.text).catch((e) => {
.catch(() => undefined) log.error("Failed to write models cache", { error: e })
if (snapshot) return snapshot })
if (Flag.OPENCODE_DISABLE_MODELS_FETCH) return {} }
return Flock.withLock(`models-dev:${filepath}`, async () => { return JSON.parse(result2.text)
const result = await Filesystem.readJson(Flag.OPENCODE_MODELS_PATH ?? filepath).catch(() => {}) })
if (result) return result })
const result2 = await fetchApi()
if (result2.ok) { export async function get() {
await Filesystem.write(filepath, result2.text).catch((e) => { const result = await Data()
log.error("Failed to write models cache", { error: e }) return result as Record<string, Provider>
}) }
}
return JSON.parse(result2.text) export async function refresh(force = false) {
if (skip(force)) return Data.reset()
await Flock.withLock(`models-dev:${filepath}`, async () => {
if (skip(force)) return Data.reset()
const result = await fetchApi()
if (!result.ok) return
await Filesystem.write(filepath, result.text)
Data.reset()
}).catch((e) => {
log.error("Failed to fetch models.dev", {
error: e,
}) })
}) })
export async function get() {
const result = await Data()
return result as Record<string, Provider>
}
export async function refresh(force = false) {
if (skip(force)) return ModelsDev.Data.reset()
await Flock.withLock(`models-dev:${filepath}`, async () => {
if (skip(force)) return ModelsDev.Data.reset()
const result = await fetchApi()
if (!result.ok) return
await Filesystem.write(filepath, result.text)
ModelsDev.Data.reset()
}).catch((e) => {
log.error("Failed to fetch models.dev", {
error: e,
})
})
}
} }
if (!Flag.OPENCODE_DISABLE_MODELS_FETCH && !process.argv.includes("--get-yargs-completions")) { if (!Flag.OPENCODE_DISABLE_MODELS_FETCH && !process.argv.includes("--get-yargs-completions")) {
void ModelsDev.refresh() void refresh()
setInterval( setInterval(
async () => { async () => {
await ModelsDev.refresh() await refresh()
}, },
60 * 1000 * 60, 60 * 1000 * 60,
).unref() ).unref()
+2 -2
View File
@@ -10,7 +10,7 @@ import { Hash } from "@opencode-ai/shared/util/hash"
import { Plugin } from "../plugin" import { Plugin } from "../plugin"
import { NamedError } from "@opencode-ai/shared/util/error" import { NamedError } from "@opencode-ai/shared/util/error"
import { type LanguageModelV3 } from "@ai-sdk/provider" import { type LanguageModelV3 } from "@ai-sdk/provider"
import { ModelsDev } from "./models" import * as ModelsDev from "./models"
import { Auth } from "../auth" import { Auth } from "../auth"
import { Env } from "../env" import { Env } from "../env"
import { Instance } from "../project/instance" import { Instance } from "../project/instance"
@@ -55,7 +55,7 @@ import {
} from "gitlab-ai-provider" } from "gitlab-ai-provider"
import { fromNodeProviderChain } from "@aws-sdk/credential-providers" import { fromNodeProviderChain } from "@aws-sdk/credential-providers"
import { GoogleAuth } from "google-auth-library" import { GoogleAuth } from "google-auth-library"
import { ProviderTransform } from "./transform" import * as ProviderTransform from "./transform"
import { Installation } from "../installation" import { Installation } from "../installation"
import { ModelID, ProviderID } from "./schema" import { ModelID, ProviderID } from "./schema"
File diff suppressed because it is too large Load Diff
@@ -1,4 +1,4 @@
import { ProviderAuth } from "@/provider/auth" import { ProviderAuth } from "@/provider"
import { Effect, Layer } from "effect" import { Effect, Layer } from "effect"
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
@@ -3,8 +3,8 @@ import { describeRoute, validator, resolver } from "hono-openapi"
import z from "zod" import z from "zod"
import { Config } from "../../config" import { Config } from "../../config"
import { Provider } from "../../provider" import { Provider } from "../../provider"
import { ModelsDev } from "../../provider/models" import { ModelsDev } from "../../provider"
import { ProviderAuth } from "../../provider/auth" import { ProviderAuth } from "../../provider"
import { ProviderID } from "../../provider/schema" import { ProviderID } from "../../provider/schema"
import { AppRuntime } from "../../effect/app-runtime" import { AppRuntime } from "../../effect/app-runtime"
import { mapValues } from "remeda" import { mapValues } from "remeda"
+1 -1
View File
@@ -5,7 +5,7 @@ import * as Stream from "effect/Stream"
import { streamText, wrapLanguageModel, type ModelMessage, type Tool, tool, jsonSchema } from "ai" import { streamText, wrapLanguageModel, type ModelMessage, type Tool, tool, jsonSchema } from "ai"
import { mergeDeep, pipe } from "remeda" import { mergeDeep, pipe } from "remeda"
import { GitLabWorkflowLanguageModel } from "gitlab-ai-provider" import { GitLabWorkflowLanguageModel } from "gitlab-ai-provider"
import { ProviderTransform } from "@/provider/transform" import { ProviderTransform } from "@/provider"
import { Config } from "@/config" import { Config } from "@/config"
import { Instance } from "@/project/instance" import { Instance } from "@/project/instance"
import type { Agent } from "@/agent/agent" import type { Agent } from "@/agent/agent"
+1 -1
View File
@@ -8,7 +8,7 @@ import { Snapshot } from "@/snapshot"
import { SyncEvent } from "../sync" import { SyncEvent } from "../sync"
import { Database, NotFoundError, and, desc, eq, inArray, lt, or } from "@/storage" import { Database, NotFoundError, and, desc, eq, inArray, lt, or } from "@/storage"
import { MessageTable, PartTable, SessionTable } from "./session.sql" import { MessageTable, PartTable, SessionTable } from "./session.sql"
import { ProviderError } from "@/provider/error" import { ProviderError } from "@/provider"
import { iife } from "@/util/iife" import { iife } from "@/util/iife"
import { errorMessage } from "@/util/error" import { errorMessage } from "@/util/error"
import type { SystemError } from "bun" import type { SystemError } from "bun"
+1 -1
View File
@@ -1,6 +1,6 @@
import type { Config } from "@/config" import type { Config } from "@/config"
import type { Provider } from "@/provider" import type { Provider } from "@/provider"
import { ProviderTransform } from "@/provider/transform" import { ProviderTransform } from "@/provider"
import type { MessageV2 } from "./message-v2" import type { MessageV2 } from "./message-v2"
const COMPACTION_BUFFER = 20_000 const COMPACTION_BUFFER = 20_000
+1 -1
View File
@@ -12,7 +12,7 @@ import { ModelID, ProviderID } from "../provider/schema"
import { type Tool as AITool, tool, jsonSchema, type ToolExecutionOptions, asSchema } from "ai" import { type Tool as AITool, tool, jsonSchema, type ToolExecutionOptions, asSchema } from "ai"
import { SessionCompaction } from "./compaction" import { SessionCompaction } from "./compaction"
import { Bus } from "../bus" import { Bus } from "../bus"
import { ProviderTransform } from "../provider/transform" import { ProviderTransform } from "../provider"
import { SystemPrompt } from "./system" import { SystemPrompt } from "./system"
import { Instruction } from "./instruction" import { Instruction } from "./instruction"
import { Plugin } from "../plugin" import { Plugin } from "../plugin"
@@ -4,7 +4,7 @@ import fs from "fs/promises"
import { Effect } from "effect" import { Effect } from "effect"
import { tmpdir } from "../fixture/fixture" import { tmpdir } from "../fixture/fixture"
import { Instance } from "../../src/project/instance" import { Instance } from "../../src/project/instance"
import { ProviderAuth } from "../../src/provider/auth" import { ProviderAuth } from "../../src/provider"
import { ProviderID } from "../../src/provider/schema" import { ProviderID } from "../../src/provider/schema"
describe("plugin.auth-override", () => { describe("plugin.auth-override", () => {
@@ -6,7 +6,7 @@ import { tmpdir } from "../fixture/fixture"
import { Global } from "../../src/global" import { Global } from "../../src/global"
import { Instance } from "../../src/project/instance" import { Instance } from "../../src/project/instance"
import { Plugin } from "../../src/plugin/index" import { Plugin } from "../../src/plugin/index"
import { ModelsDev } from "../../src/provider/models" import { ModelsDev } from "../../src/provider"
import { Provider } from "../../src/provider" import { Provider } from "../../src/provider"
import { ProviderID, ModelID } from "../../src/provider/schema" import { ProviderID, ModelID } from "../../src/provider/schema"
import { Filesystem } from "../../src/util" import { Filesystem } from "../../src/util"
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test" import { describe, expect, test } from "bun:test"
import { ProviderTransform } from "../../src/provider/transform" import { ProviderTransform } from "../../src/provider"
import { ModelID, ProviderID } from "../../src/provider/schema" import { ModelID, ProviderID } from "../../src/provider/schema"
describe("ProviderTransform.options - setCacheKey", () => { describe("ProviderTransform.options - setCacheKey", () => {
+2 -2
View File
@@ -7,8 +7,8 @@ import { makeRuntime } from "../../src/effect/run-service"
import { LLM } from "../../src/session/llm" import { LLM } from "../../src/session/llm"
import { Instance } from "../../src/project/instance" import { Instance } from "../../src/project/instance"
import { Provider } from "../../src/provider" import { Provider } from "../../src/provider"
import { ProviderTransform } from "../../src/provider/transform" import { ProviderTransform } from "../../src/provider"
import { ModelsDev } from "../../src/provider/models" import { ModelsDev } from "../../src/provider"
import { ProviderID, ModelID } from "../../src/provider/schema" import { ProviderID, ModelID } from "../../src/provider/schema"
import { Filesystem } from "../../src/util" import { Filesystem } from "../../src/util"
import { tmpdir } from "../fixture/fixture" import { tmpdir } from "../fixture/fixture"