From 4a17c7e14334401888749bca1db221dbbe8dd8d3 Mon Sep 17 00:00:00 2001 From: PabloGNU Date: Tue, 7 Apr 2026 14:08:56 +0200 Subject: [PATCH] feat(auth): support embedded profile in model ID - parseModel extracts authProfile from providerID if contains ':' - Profile embedded in model string has highest priority - Precedence: embedded > config authProfile > env var > default - Auth.get supports direct lookup of provider:profile keys - Agent.Info model supports optional authProfile field --- packages/opencode/src/agent/agent.ts | 1 + packages/opencode/src/auth/index.ts | 6 +++- packages/opencode/src/provider/provider.ts | 36 ++++++++++++++-------- 3 files changed, 29 insertions(+), 14 deletions(-) diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 0c6fe6ec91..86ab11698b 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -39,6 +39,7 @@ export namespace Agent { .object({ modelID: ModelID.zod, providerID: ProviderID.zod, + authProfile: z.string().optional(), }) .optional(), variant: z.string().optional(), diff --git a/packages/opencode/src/auth/index.ts b/packages/opencode/src/auth/index.ts index 775606e513..a6df231ab2 100644 --- a/packages/opencode/src/auth/index.ts +++ b/packages/opencode/src/auth/index.ts @@ -73,7 +73,11 @@ export namespace Auth { if (providerID in allData) return allData[providerID] const withSlash = providerID.endsWith("/") ? providerID.slice(0, -1) : providerID + "/" if (withSlash in allData) return allData[withSlash] - // Multi-profile support: try normalized key + // Multi-profile support: if key has embedded profile (provider:profile), try direct + if (providerID.includes(":")) { + if (providerID in allData) return allData[providerID] + } + // Multi-profile support: try normalized key with :default const withProfile = normalizeKey(providerID) if (withProfile in allData) return allData[withProfile] const bare = providerID.replace(/\/+$/, "") diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 314ee0bfa7..5de6aa0b05 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -820,6 +820,7 @@ export namespace Provider { .object({ id: ModelID.zod, providerID: ProviderID.zod, + authProfile: z.string().optional(), api: z.object({ id: z.string(), url: z.string(), @@ -1030,10 +1031,12 @@ export namespace Provider { [providerID: string]: CustomDiscoverModels } = {} // resolveProfile returns the auth profile for a provider with precedence: - // 1. opencode.json → provider[providerID].options.authProfile - // 2. {PROVIDER_ID_UPPERCASE}_AUTH_PROFILE env var (dots replaced by underscores) - // 3. "default" - function resolveProfile(providerID: string): string { + // 1. Embedded profile from model string (e.g., "provider:profile/model") - highest priority + // 2. opencode.json → provider[providerID].options.authProfile + // 3. {PROVIDER_ID_UPPERCASE}_AUTH_PROFILE env var (dots replaced by underscores) + // 4. "default" + function resolveProfile(providerID: string, profileOverride?: string): string { + if (profileOverride) return profileOverride const cfg = providers[providerID as ProviderID] if (cfg?.options?.authProfile) return cfg.options.authProfile const envKey = `${providerID.toUpperCase().replace(/\./g, "_")}_AUTH_PROFILE` @@ -1043,15 +1046,13 @@ export namespace Provider { } const dep = { - auth: (id: string) => { - const profile = resolveProfile(id) + auth: (id: string, profileOverride?: string) => { + const profile = resolveProfile(id, profileOverride) + const key = profile !== "default" ? `${id.replace(/\/+$/, "")}:${profile}` : id if (profile !== "default") { log.info(`Using auth profile: ${profile}`) - const key = `${id.replace(/\/+$/, "")}:${profile}` - return auth.get(key).pipe(Effect.orDie) } - // For default profile, pass original ID to preserve backward compat - return auth.get(id).pipe(Effect.orDie) + return auth.get(key).pipe(Effect.orDie) }, config: () => config.get(), } @@ -1705,10 +1706,19 @@ export namespace Provider { ) } - export function parseModel(model: string) { - const [providerID, ...rest] = model.split("/") + export function parseModel(model: string): { providerID: ProviderID; modelID: ModelID; authProfile?: string } { + const [first, ...rest] = model.split("/") + // Check if providerID contains a profile (e.g., "provider:profile") + if (first.includes(":")) { + const [providerID, authProfile] = first.split(":") + return { + providerID: ProviderID.make(providerID), + modelID: ModelID.make(rest.join("/")), + authProfile, + } + } return { - providerID: ProviderID.make(providerID), + providerID: ProviderID.make(first), modelID: ModelID.make(rest.join("/")), } }