Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
48b74f8
feat(opencode): add LiteLLM discovery module with /model/info and /mo…
balcsida Feb 20, 2026
f64466a
feat(opencode): add litellm provider seeding and custom loader
balcsida Feb 20, 2026
0900f6d
feat(opencode): add litellm-specific reasoning transform variants
balcsida Feb 20, 2026
1870e39
fix(opencode): add explicit litellm providerID check for proxy detection
balcsida Feb 20, 2026
966a69c
feat(opencode): allow interactive litellm provider configuration
balcsida Feb 20, 2026
3babfc2
feat(opencode): add litellm multi-step connect flow in TUI
balcsida Feb 20, 2026
c86b5f1
feat(app): add litellm connect dialog with base URL and API key fields
balcsida Feb 20, 2026
f588015
refactor: clean up litellm code to follow style guide
balcsida Feb 20, 2026
a99d6a9
fix(opencode): use global config update for litellm base URL persistence
balcsida Feb 20, 2026
56b78e5
feat(opencode): store underlying model in litellm model options
balcsida Feb 20, 2026
44da13e
fix(opencode): detect litellm Claude models via underlying model info
balcsida Feb 20, 2026
ce00080
fix(opencode): use snake_case budget_tokens for litellm thinking vari…
balcsida Feb 20, 2026
c812b79
fix(opencode): select correct system prompt for litellm Claude models
balcsida Feb 20, 2026
db4e4ed
fix(opencode): filter underlyingModel from litellm provider options
balcsida Feb 20, 2026
0497fab
feat(opencode): enable prompt caching for litellm Claude models
balcsida Feb 20, 2026
25ad7db
fix(opencode): add 10s timeout to bun info registry check
balcsida Feb 21, 2026
e8ce6a8
refactor(opencode): replace try/catch with .catch() in litellm code
balcsida Feb 21, 2026
0988ce8
fix(opencode): use branded types for litellm ModelID and ProviderID
balcsida Mar 13, 2026
f511677
fix(ci): retry bun install to handle transient Windows linking failures
balcsida Mar 13, 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
14 changes: 13 additions & 1 deletion .github/actions/setup-bun/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,17 @@ runs:
shell: bash

- name: Install dependencies
run: bun install
shell: bash
run: |
attempts=3
for i in $(seq 1 $attempts); do
if bun install; then
exit 0
fi
echo "::warning::bun install attempt $i/$attempts failed"
if [ "$i" -lt "$attempts" ]; then
sleep 5
fi
done
echo "::error::bun install failed after $attempts attempts"
exit 1
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 @@ -308,6 +308,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 @@ -478,6 +540,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
1 change: 1 addition & 0 deletions packages/opencode/src/bun/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export namespace PackageRegistry {
export async function info(pkg: string, field: string, cwd?: string): Promise<string | null> {
const { code, stdout, stderr } = await Process.run([which(), "info", pkg, field], {
cwd,
timeout: 10_000,
env: {
...process.env,
BUN_BE_BUN: "1",
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 @@ -85,6 +85,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 @@ -257,3 +260,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