Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/app/src/components/dialog-select-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const DialogSelectProvider: Component = () => {
if (id === "openai") return language.t("dialog.provider.openai.note")
if (id.startsWith("github-copilot")) return language.t("dialog.provider.copilot.note")
if (id === "opencode-go") return language.t("dialog.provider.opencodeGo.tagline")
if (id === "litellm") return language.t("dialog.provider.litellm.note")
}

return (
Expand Down
1 change: 1 addition & 0 deletions packages/app/src/components/settings-providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const PROVIDER_NOTES = [
{ match: (id: string) => id === "google", key: "dialog.provider.google.note" },
{ match: (id: string) => id === "openrouter", key: "dialog.provider.openrouter.note" },
{ match: (id: string) => id === "vercel", key: "dialog.provider.vercel.note" },
{ match: (id: string) => id === "litellm", key: "dialog.provider.litellm.note" },
] as const

export const SettingsProviders: Component = () => {
Expand Down
1 change: 1 addition & 0 deletions packages/app/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ export const dict = {
"dialog.provider.google.note": "Gemini models for fast, structured responses",
"dialog.provider.openrouter.note": "Access all supported models from one provider",
"dialog.provider.vercel.note": "Unified access to AI models with smart routing",
"dialog.provider.litellm.note": "Unified proxy for 100+ LLMs with load balancing and spend tracking",

"dialog.model.select.title": "Select model",
"dialog.model.search.placeholder": "Search models",
Expand Down
108 changes: 108 additions & 0 deletions packages/opencode/src/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -858,6 +858,79 @@ function custom(dep: CustomDep): Record<string, CustomLoader> {
},
},
}),
litellm: Effect.fnUntraced(function* (input: Info) {
const env = yield* dep.env()
const auth = yield* dep.auth(input.id)

const apiKey = iife(() => {
if (input.env.some((item) => env[item])) return input.env.map((item) => env[item]).find(Boolean)
if (auth?.type === "api") return auth.key
if (input.options?.apiKey) return input.options.apiKey
return undefined
})

const baseURL = iife(() => {
if (input.options?.baseURL) return input.options.baseURL
if (env["LITELLM_BASE_URL"]) return env["LITELLM_BASE_URL"]
return undefined
})

if (!baseURL) return { autoload: false }

return {
autoload: true,
options: {
...(apiKey ? { apiKey } : {}),
baseURL,
},
async discoverModels(): Promise<Record<string, Model>> {
try {
const url = `${baseURL.replace(/\/+$/, "")}/v1/models`
const headers: Record<string, string> = {
"User-Agent": `opencode/${InstallationVersion} litellm (${os.platform()} ${os.release()}; ${os.arch()})`,
}
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`
const res = await fetch(url, { headers })
if (!res.ok) return {}
const body = (await res.json()) as { data?: Array<{ id: string; created?: number }> }
if (!Array.isArray(body.data)) return {}
const models: Record<string, Model> = {}
for (const entry of body.data) {
const id = entry.id
models[id] = {
id: ModelID.make(id),
providerID: ProviderID.make("litellm"),
name: id,
api: {
id,
url: baseURL,
npm: "@ai-sdk/openai-compatible",
},
status: "active",
headers: {},
options: {},
cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
limit: { context: 128_000, output: 16_384 },
capabilities: {
temperature: true,
reasoning: false,
attachment: true,
toolcall: true,
input: { text: true, audio: false, image: true, video: false, pdf: false },
output: { text: true, audio: false, image: false, video: false, pdf: false },
interleaved: false,
},
release_date: "",
variants: {},
}
}
return models
} catch {
return {}
}
},
}
}),
}
}

Expand Down Expand Up @@ -1433,6 +1506,25 @@ export const layer = Layer.effect(
mergeProvider(providerID, patch)
}

// Seed LiteLLM into database if env vars or stored auth indicate it should be available
// but it's not already in models.dev or config. This lets the custom loader and
// model discovery run without requiring a manual config entry.
if (!database["litellm"] && !disabled.has(ProviderID.make("litellm"))) {
const litellmBaseURL = envs["LITELLM_BASE_URL"]
const litellmKey = envs["LITELLM_API_KEY"]
const litellmAuth = auths["litellm"]
if (litellmBaseURL || litellmKey || litellmAuth) {
database["litellm"] = {
id: ProviderID.make("litellm"),
name: "LiteLLM",
source: "env",
env: ["LITELLM_API_KEY"],
options: {},
models: {},
}
}
}

for (const [id, fn] of Object.entries(custom(dep))) {
const providerID = ProviderID.make(id)
if (disabled.has(providerID)) continue
Expand Down Expand Up @@ -1478,6 +1570,22 @@ export const layer = Layer.effect(
})
}

const litellm = ProviderID.make("litellm")
if (discoveryLoaders[litellm] && providers[litellm] && isProviderAllowed(litellm)) {
yield* Effect.promise(async () => {
try {
const discovered = await discoveryLoaders[litellm]()
for (const [modelID, model] of Object.entries(discovered)) {
if (!providers[litellm].models[modelID]) {
providers[litellm].models[modelID] = model
}
}
} catch (e) {
log.warn("state discovery error", { id: "litellm", error: e })
}
})
}

for (const [id, provider] of Object.entries(providers)) {
const providerID = ProviderID.make(id)
if (!isProviderAllowed(providerID)) {
Expand Down
1 change: 1 addition & 0 deletions packages/opencode/src/provider/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const ProviderID = providerIdSchema.pipe(
openrouter: schema.make("openrouter"),
mistral: schema.make("mistral"),
gitlab: schema.make("gitlab"),
litellm: schema.make("litellm"),
})),
)

Expand Down
3 changes: 3 additions & 0 deletions packages/ui/src/components/provider-icons/sprite.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions packages/ui/src/components/provider-icons/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const iconNames = [
"ovhcloud",
"openrouter",
"llmgateway",
"litellm",
"opencode",
"opencode-go",
"openai",
Expand Down
Loading