Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
012841c
Dynamic model discovery from OpenAI-compatible providers
dmitryryabkov Mar 15, 2026
97476f4
typecheck fixes
dmitryryabkov Mar 15, 2026
09988ea
Merge branch 'dev' into dynamic-model-discovery
dmitryryabkov Mar 20, 2026
cc37786
typecheck fixes afther the merge from upstream
dmitryryabkov Mar 20, 2026
0deb244
Merge branch 'dev' into dynamic-model-discovery
dmitryryabkov Mar 20, 2026
4c3bbcd
Merge branch 'dev' into dynamic-model-discovery
dmitryryabkov Mar 21, 2026
da2c5ef
Merge branch 'dev' into dynamic-model-discovery
dmitryryabkov Mar 22, 2026
2d3916b
Merge branch 'dev' into dynamic-model-discovery
dmitryryabkov Mar 22, 2026
f47f62e
Merge branch 'dev' into dynamic-model-discovery
dmitryryabkov Mar 23, 2026
6ea1bfa
Merge branch 'dev' into dynamic-model-discovery
dmitryryabkov Mar 24, 2026
3aa949e
Merge branch 'dev' into dynamic-model-discovery
dmitryryabkov Mar 25, 2026
b9b6eb4
Merge branch 'dev' into dynamic-model-discovery
dmitryryabkov Mar 25, 2026
e9bbb9a
Merge branch 'dev' into dynamic-model-discovery
dmitryryabkov Mar 25, 2026
c04a5be
Merge branch 'dev' into dynamic-model-discovery
dmitryryabkov Mar 26, 2026
a8675d5
Merge branch 'dev' into dynamic-model-discovery
dmitryryabkov Mar 26, 2026
c5b0e44
Merge branch 'dev' into dynamic-model-discovery
dmitryryabkov Mar 26, 2026
120d6ef
Merge branch 'dev' into dynamic-model-discovery
dmitryryabkov Mar 27, 2026
e3060e8
Merge branch 'dev' into dynamic-model-discovery
dmitryryabkov Mar 29, 2026
75a2b87
Merge branch 'dev' into dynamic-model-discovery
dmitryryabkov Apr 1, 2026
0f4aae9
Fix the merge issues
dmitryryabkov Apr 1, 2026
9630360
Merge branch 'dev' into dynamic-model-discovery
dmitryryabkov Apr 1, 2026
1af42de
Merge branch 'dev' into dynamic-model-discovery
dmitryryabkov Apr 2, 2026
22b0047
Merge branch 'dev' into dynamic-model-discovery
dmitryryabkov Apr 2, 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
1 change: 1 addition & 0 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -806,6 +806,7 @@ export namespace Config {
}),
)
.optional(),
dynamicModelList: z.boolean().optional().describe("Enable automatic model discovery from OpenAI-compatible /models endpoint"),
options: z
.object({
apiKey: z.string().optional(),
Expand Down
135 changes: 135 additions & 0 deletions packages/opencode/src/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -788,6 +788,137 @@ export namespace Provider {
},
}

/**
* Populate the provider models dynamically using provider config
* Returns models or emty object if fails
*/
async function populateDynamicModels(providerID: string, provider: any): Promise<Record<string, Model>> {
// Get base URL from config or thrown an exception
const baseURL = provider.options?.baseURL
if (!baseURL) {
log.error("Missing baseURL for dynamic model discovery", { providerID })
throw new InitError({ providerID: providerID })
}

// Get auth credentials
const key = provider.options?.apiKey
const auth = key ? { type: "api", key: key } : await Auth.get(providerID)

// Discover models
const discoveredModels = await discoverModelsFromEndpoint(providerID, baseURL, auth?.type === "api" ? auth : null)
return discoveredModels
}

/**
* Discover models from OpenAI-compatible /models endpoint
* Returns discovered models or empty object if discovery fails
*/
async function discoverModelsFromEndpoint(
providerID: string,
baseURL: string,
auth: any | null,
): Promise<Record<string, Model>> {
const models: Record<string, Model> = {}

try {
const headers: Record<string, string> = {}
if (auth?.type === "api" && auth.key) {
headers.Authorization = `Bearer ${auth.key}`
}

const response = await fetch(`${baseURL}/models`, {
headers,
signal: AbortSignal.timeout(10000),
})

if (!response.ok) {
log.warn("Failed to discover models", {
providerID,
baseURL,
status: response.status,
})
return models
}

const json = await response.json()

// Handle OpenAI format: { data: [{ id, ... }] }
const data = json.data
if (!Array.isArray(data)) {
log.warn("Unexpected /models response format", { providerID, format: typeof data })
return models
}

for (const modelData of data) {
const modelID = modelData.id
if (!modelID || typeof modelID !== "string") continue

// Extract context length from various possible fields
const contextLength =
modelData.max_context_length ??
modelData.context_length ??
modelData.contextWindow ??
modelData.max_tokens ??
131072

const context = Math.max(contextLength, 8192) // Floor at 8k for stability
const output = Math.min(Math.floor(context / 4), 16384)

// Check for small context warning
if (context < 32768) {
log.warn("Model has small context limit", {
providerID,
modelID,
context,
})
}

models[modelID] = {
id: ModelID.make(modelID),
providerID: ProviderID.make(providerID),
name: modelData.name ?? modelID,
family: modelData.family ?? "",
api: {
id: modelID,
url: baseURL,
npm: "@ai-sdk/openai-compatible",
},
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,
},
cost: { input: 0, output: 0, cache: { read: 0, write: 0 } },
limit: { context, output },
headers: {},
options: {},
release_date: modelData.created ?? "",
status: modelData.status?.value ?? "active",
}
}

if (Object.keys(models).length > 0) {
log.info("Discovered models", {
providerID,
count: Object.keys(models).length,
models: Object.keys(models),
})
}
} catch (error) {
log.warn("Failed to discover models", {
providerID,
url: baseURL,
error: error,
})
}

return models
}

export const Model = z
.object({
id: ModelID.zod,
Expand Down Expand Up @@ -1189,6 +1320,10 @@ export namespace Provider {
if (provider.env) partial.env = provider.env
if (provider.name) partial.name = provider.name
if (provider.options) partial.options = provider.options
const hasExplicitModels = Object.keys(provider.models ?? {}).length > 0
if (!hasExplicitModels && provider.dynamicModelList) {
partial.models = yield* Effect.promise(() => populateDynamicModels(providerID, provider))
}
mergeProvider(providerID, partial)
}

Expand Down
Loading