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/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -997,6 +997,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
145 changes: 142 additions & 3 deletions packages/opencode/src/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -428,9 +428,9 @@ export namespace Provider {

const location = String(
provider.options?.location ??
Env.get("GOOGLE_VERTEX_LOCATION") ??
Env.get("GOOGLE_CLOUD_LOCATION") ??
Env.get("VERTEX_LOCATION") ??
Env.get("GOOGLE_VERTEX_LOCATION") ??
Env.get("GOOGLE_CLOUD_LOCATION") ??
Env.get("VERTEX_LOCATION") ??
"us-central1",
)

Expand Down Expand Up @@ -670,6 +670,141 @@ 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 @@ -1070,6 +1205,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 = await populateDynamicModels(providerID, provider)
}
mergeProvider(providerID, partial)
}

Expand Down
Loading
Loading