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
a69fc62
feat: add Unbound as a provider
vigneshsubbiah16 Jan 10, 2026
bbdf00a
fix: parse Unbound API response correctly for model parameters
vigneshsubbiah16 Jan 10, 2026
4972a09
feat: add X-Unbound-Metadata header and improve model parsing
vigneshsubbiah16 Jan 10, 2026
b0081bf
fix: address PR review feedback
vigneshsubbiah16 Jan 10, 2026
b9a1052
fix: use official Unbound logo
vigneshsubbiah16 Jan 10, 2026
cde7eb0
refactor: simplify Unbound provider code and add tests
vigneshsubbiah16 Jan 10, 2026
6e8ab77
Merge branch 'dev' into feat/add-unbound-provider
vigneshsubbiah16 Jan 11, 2026
79e2f0c
Merge branch 'dev' into feat/add-unbound-provider
vigneshsubbiah16 Jan 12, 2026
0d061b9
Merge branch 'dev' into feat/add-unbound-provider
vigneshsubbiah16 Jan 13, 2026
5a3a694
Merge branch 'dev' into feat/add-unbound-provider
vigneshsubbiah16 Jan 13, 2026
e508069
Merge branch 'dev' into feat/add-unbound-provider
vigneshsubbiah16 Jan 13, 2026
0d2ddd4
Merge branch 'dev' into feat/add-unbound-provider
vigneshsubbiah16 Jan 13, 2026
c8f8c16
Merge branch 'dev' into feat/add-unbound-provider
vigneshsubbiah16 Jan 14, 2026
5ccd14d
Merge branch 'dev' into feat/add-unbound-provider
vigneshsubbiah16 Jan 14, 2026
a928f29
Merge branch 'dev' into feat/add-unbound-provider
vigneshsubbiah16 Jan 14, 2026
21f59b8
Merge branch 'dev' into feat/add-unbound-provider
vigneshsubbiah16 Jan 15, 2026
b528484
Merge branch 'dev' into feat/add-unbound-provider
vigneshsubbiah16 Jan 16, 2026
1cc3ecf
Merge branch 'dev' into feat/add-unbound-provider
vigneshsubbiah16 Jan 18, 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
121 changes: 121 additions & 0 deletions packages/opencode/src/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,92 @@ export namespace Provider {
},
}
},
unbound: async (input) => {
const config = await Config.get()
const providerConfig = config.provider?.["unbound"]

// Get API key from env, auth storage, or config (consistent with cloudflare-ai-gateway pattern)
const apiKey = await (async () => {
const envKey = input.env.map((key) => Env.get(key)).find(Boolean)
if (envKey) return envKey
const auth = await Auth.get(input.id)
if (auth?.type === "api") return auth.key
return providerConfig?.options?.apiKey
})()

if (!apiKey) return { autoload: false }

const baseURL = providerConfig?.options?.baseURL ?? "https://api.getunbound.ai/v1"

// Fetch available models from Unbound gateway
try {
const response = await fetch(`${baseURL}/models`, {
headers: { Authorization: `Bearer ${apiKey}` },
signal: AbortSignal.timeout(10000),
})

if (response.ok) {
const data = await response.json()
const models = data.data ?? data.models ?? []

if (models.length > 0) {
delete input.models["default"]
for (const model of models) {
const modelId = model.id ?? model.name
const params = model.parameters ?? model
const pricing = model.pricing ?? {}
const supportsImages = params.supports_images ?? params.supportsImages ?? false

input.models[modelId] = {
id: modelId,
providerID: "unbound",
name: model.name ?? modelId,
api: { id: modelId, url: baseURL, npm: "@ai-sdk/openai-compatible" },
status: "active",
headers: {},
options: { supportsPromptCaching: params.supports_prompt_caching ?? params.supportsPromptCaching ?? false },
cost: {
input: parseFloat(pricing.input_token_price ?? pricing.inputTokenPrice) || 0,
output: parseFloat(pricing.output_token_price ?? pricing.outputTokenPrice) || 0,
cache: {
read: parseFloat(pricing.cache_read_price ?? pricing.cacheReadPrice) || 0,
write: parseFloat(pricing.cache_write_price ?? pricing.cacheWritePrice) || 0,
},
},
limit: {
context: params.context_window ?? params.contextWindow ?? 128000,
output: params.max_tokens ?? params.maxTokens ?? 4096,
},
capabilities: {
temperature: true,
reasoning: false,
attachment: supportsImages,
toolcall: true,
input: { text: true, audio: false, image: supportsImages, video: false, pdf: false },
output: { text: true, audio: false, image: false, video: false, pdf: false },
interleaved: false,
},
release_date: new Date().toISOString().split("T")[0],
variants: {},
}
}
}
} else {
log.warn("Failed to fetch Unbound models", { status: response.status, statusText: response.statusText })
}
} catch (e) {
log.warn("Failed to fetch Unbound models, using default", { error: e })
}

return {
autoload: true,
options: {
headers: {
"X-Unbound-Metadata": JSON.stringify({ labels: [{ key: "app", value: "opencode" }] }),
},
},
}
},
}

export const Model = z
Expand Down Expand Up @@ -704,6 +790,41 @@ export namespace Provider {
}
}

// Add Unbound provider (AI Gateway) - models are fetched dynamically via custom loader
if (!database["unbound"]) {
database["unbound"] = {
id: "unbound",
name: "Unbound",
source: "custom",
env: ["UNBOUND_API_KEY"],
options: {},
models: {
default: {
id: "default",
providerID: "unbound",
name: "Default Model",
api: { id: "default", url: "https://api.getunbound.ai/v1", npm: "@ai-sdk/openai-compatible" },
status: "active",
headers: {},
options: {},
cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
limit: { context: 128000, output: 4096 },
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: "2024-01-01",
variants: {},
},
},
}
}

function mergeProvider(providerID: string, provider: Partial<Info>) {
const existing = providers[providerID]
if (existing) {
Expand Down
Loading