Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
35d529c
feat(opencode): add LiteLLM discovery module with /model/info and /mo…
balcsida Feb 20, 2026
f7cbcee
feat(opencode): add litellm provider seeding and custom loader
balcsida Feb 20, 2026
2a81a94
feat(opencode): add litellm-specific reasoning transform variants
balcsida Feb 20, 2026
ce92937
fix(opencode): add explicit litellm providerID check for proxy detection
balcsida Feb 20, 2026
99c312f
feat(opencode): allow interactive litellm provider configuration
balcsida Feb 20, 2026
d859774
feat(opencode): add litellm multi-step connect flow in TUI
balcsida Feb 20, 2026
a551928
feat(app): add litellm connect dialog with base URL and API key fields
balcsida Feb 20, 2026
f853c63
refactor: clean up litellm code to follow style guide
balcsida Feb 20, 2026
09ea704
fix(opencode): use global config update for litellm base URL persistence
balcsida Feb 20, 2026
7394dc6
feat(opencode): store underlying model in litellm model options
balcsida Feb 20, 2026
1049e5e
fix(opencode): detect litellm Claude models via underlying model info
balcsida Feb 20, 2026
9bbd29a
fix(opencode): use snake_case budget_tokens for litellm thinking vari…
balcsida Feb 20, 2026
7ea81ab
fix(opencode): select correct system prompt for litellm Claude models
balcsida Feb 20, 2026
351dcd0
fix(opencode): filter underlyingModel from litellm provider options
balcsida Feb 20, 2026
f2dee7c
feat(opencode): enable prompt caching for litellm Claude models
balcsida Feb 20, 2026
6783600
fix(opencode): add 10s timeout to bun info registry check
balcsida Feb 21, 2026
d3e6960
refactor(opencode): replace try/catch with .catch() in litellm code
balcsida Feb 21, 2026
ee81b6b
fix(opencode): clean up streams after killing timed-out bun info
balcsida Feb 22, 2026
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
65 changes: 65 additions & 0 deletions packages/app/src/components/dialog-connect-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,68 @@ export function DialogConnectProvider(props: { provider: string }) {
)
}

function LiteLLMAuthView() {
const [formStore, setFormStore] = createStore({
baseURL: "",
apiKey: "",
})

async function handleSubmit(e: SubmitEvent) {
e.preventDefault()

const data = new FormData(e.currentTarget as HTMLFormElement)
const url = (data.get("baseURL") as string)?.trim() || "http://localhost:4000"
const key = (data.get("apiKey") as string)?.trim()

await globalSDK.client.global.config.update({
config: {
provider: {
litellm: {
options: { baseURL: url },
},
},
},
})
if (key) {
await globalSDK.client.auth.set({
providerID: props.provider,
auth: { type: "api", key },
})
}
await complete()
}

return (
<div class="flex flex-col gap-6">
<div class="text-14-regular text-text-base">
Connect to a LiteLLM proxy server. Models will be discovered automatically.
</div>
<form onSubmit={handleSubmit} class="flex flex-col items-start gap-4">
<TextField
autofocus
type="text"
label="Base URL"
placeholder="http://localhost:4000"
name="baseURL"
value={formStore.baseURL}
onChange={(v) => setFormStore("baseURL", v)}
/>
<TextField
type="text"
label="API Key (optional)"
placeholder="sk-..."
name="apiKey"
value={formStore.apiKey}
onChange={(v) => setFormStore("apiKey", v)}
/>
<Button class="w-auto" type="submit" size="large" variant="primary">
{language.t("common.submit")}
</Button>
</form>
</div>
)
}

function OAuthCodeView() {
const [formStore, setFormStore] = createStore({
value: "",
Expand Down Expand Up @@ -479,6 +541,9 @@ export function DialogConnectProvider(props: { provider: string }) {
</div>
</div>
</Match>
<Match when={method()?.type === "api" && props.provider === "litellm"}>
<LiteLLMAuthView />
</Match>
<Match when={method()?.type === "api"}>
<ApiAuthView />
</Match>
Expand Down
15 changes: 14 additions & 1 deletion packages/opencode/src/bun/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,20 @@ export namespace PackageRegistry {
},
})

const code = await result.exited
const code = await Promise.race([
result.exited,
new Promise<null>((resolve) => setTimeout(() => resolve(null), 10_000)),
])

if (code === null) {
result.kill()
result.stdout?.cancel().catch(() => {})
result.stderr?.cancel().catch(() => {})
result.unref()
log.warn("bun info timed out", { pkg, field })
return null
}

const stdout = result.stdout ? await readableStreamToText(result.stdout) : ""
const stderr = result.stderr ? await readableStreamToText(result.stderr) : ""

Expand Down
52 changes: 52 additions & 0 deletions packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ export function createDialogProviderOptions() {
}
}
if (method.type === "api") {
if (provider.id === "litellm") {
return dialog.replace(() => <LiteLLMMethod />)
}
return dialog.replace(() => <ApiMethod providerID={provider.id} title={method.label} />)
}
},
Expand Down Expand Up @@ -241,3 +244,52 @@ function ApiMethod(props: ApiMethodProps) {
/>
)
}

function LiteLLMMethod() {
const dialog = useDialog()
const sdk = useSDK()
const sync = useSync()
const { theme } = useTheme()

return (
<DialogPrompt
title="LiteLLM Base URL"
placeholder="http://localhost:4000"
description={() => (
<text fg={theme.textMuted}>Enter the base URL of your LiteLLM proxy server.</text>
)}
onConfirm={async (baseURL) => {
const url = baseURL?.trim() || "http://localhost:4000"
dialog.replace(() => (
<DialogPrompt
title="LiteLLM API Key"
placeholder="API key (optional)"
description={() => (
<text fg={theme.textMuted}>Enter the API key for your LiteLLM proxy, or leave empty if not required.</text>
)}
onConfirm={async (apiKey) => {
await sdk.client.global.config.update({
config: {
provider: {
litellm: {
options: { baseURL: url },
},
},
},
})
if (apiKey?.trim()) {
await sdk.client.auth.set({
providerID: "litellm",
auth: { type: "api", key: apiKey.trim() },
})
}
await sdk.client.instance.dispose()
await sync.bootstrap()
dialog.replace(() => <DialogModel providerID="litellm" />)
}}
/>
))
}}
/>
)
}
Loading
Loading