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
pull/21353/head
PabloGNU 2026-04-07 14:08:56 +02:00
parent 153198061d
commit 4a17c7e143
3 changed files with 29 additions and 14 deletions

View File

@ -39,6 +39,7 @@ export namespace Agent {
.object({
modelID: ModelID.zod,
providerID: ProviderID.zod,
authProfile: z.string().optional(),
})
.optional(),
variant: z.string().optional(),

View File

@ -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(/\/+$/, "")

View File

@ -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("/")),
}
}