1- import std/ [os, parsecfg, streams, strformat, strutils, terminal, uri]
1+ import std/ [os, parsecfg, streams, strformat, strutils, tables, terminal, uri]
22import types, prompts
33
44type
55 ProviderRec * = object
66 # # In-memory mirror of a [provider] section. `family` is the optional
77 # # experimental override (broad name like "glm"/"qwen"/"gpt-oss") used
88 # # to pick a system prompt; only honored when --experimental is on.
9- # # Known-good combos ignore it. `models` is the list of API model ids
10- # # this provider exposes (stored without `modelPrefix`, which carries
11- # # the shared path prefix so it doesn't repeat per model).
9+ # # Known-good combos ignore it. `models` is the list of full API model
10+ # # ids (e.g. "openai/gpt-oss-120b") as sent on the wire. `modelPrefix`
11+ # # is only populated transiently when reading old config files that
12+ # # stored a separate `model_prefix` key; it is expanded into the model
13+ # # ids on load and never written back out.
1214 name* , url* , key* , modelPrefix* , family* : string
1315 models* : seq [string ]
1416
17+ proc shortModel * (model: string ): string =
18+ # # Everything after the last `/` in a model id. This is the
19+ # # user-visible short name: `gpt-oss-120b` for `openai/gpt-oss-120b`,
20+ # # `glm-5p1` for `accounts/fireworks/models/glm-5p1`. When there is no
21+ # # slash, the model id is already a bare name and is returned as-is.
22+ let slash = model.rfind ('/' )
23+ if slash < 0 : model else : model[slash + 1 .. ^ 1 ]
24+
25+ proc shortToFull * (models: seq [string ]): Table [string , string ] =
26+ # # Maps each short model name (after the last `/`) to the full model id.
27+ # # When two full ids share the same short name — e.g. nvidia sometimes
28+ # # lists a model both as `org/model-name` and bare `model-name` — only
29+ # # the first occurrence is kept. This mirrors the display list: the
30+ # # user sees both names, picks the short one, and gets the first match.
31+ # # If genuine ambiguity arises in the future we can promote a conflict
32+ # # notice here; for now silent first-wins is the right trade-off.
33+ for m in models:
34+ let s = shortModel (m)
35+ if s notin result :
36+ result [s] = m
37+
1538proc findModel * (p: ProviderRec , name: string ): int =
39+ # # Matches by full model id or by short name (everything after the last
40+ # # `/`). Short-name matching handles `:variant <name>` from users who
41+ # # type the bare model name and old `current = provider.shortname` config
42+ # # values that haven't been rewritten yet.
1643 for i, m in p.models:
17- if m == name: return i
44+ if m == name or shortModel (m) == name : return i
1845 - 1
1946
2047var activeCurrent* : string
@@ -42,7 +69,7 @@ proc explainExperimentalGate*(p: Profile) =
4269
4370proc hasKnownGoodModel * (prov: ProviderRec ): bool =
4471 for m in prov.models:
45- if knownGoodFamily (prov.name, prov.modelPrefix & m) != " " : return true
72+ if knownGoodFamily (prov.name, m) != " " : return true
4673 false
4774
4875proc firstKnownGoodCombo * (providers: seq [ProviderRec ]): string =
@@ -53,7 +80,7 @@ proc firstKnownGoodCombo*(providers: seq[ProviderRec]): string =
5380 for pr in providers:
5481 if pr.url == " " or pr.key == " " : continue
5582 for m in pr.models:
56- if knownGoodFamily (pr.name, pr.modelPrefix & m) != " " :
83+ if knownGoodFamily (pr.name, m) != " " :
5784 return pr.name & " ." & m
5885 " "
5986
@@ -96,6 +123,15 @@ proc parseConfigFile*(path: string): (string, seq[ProviderRec]) =
96123 p.open (stream, path)
97124 proc flush () =
98125 if inProvider:
126+ # Backward compat: old configs wrote `model_prefix = "openai/"` and
127+ # stored bare names like `"gpt-oss-120b"` in `models`. Expand them
128+ # to full ids here so the rest of the codebase only ever sees full
129+ # ids. The prefix is never written back out.
130+ if prov.modelPrefix != " " :
131+ for i in 0 ..< prov.models.len:
132+ if not prov.models[i].startsWith (prov.modelPrefix):
133+ prov.models[i] = prov.modelPrefix & prov.models[i]
134+ prov.modelPrefix = " "
99135 providers.add prov
100136 prov = ProviderRec ()
101137 inProvider = false
@@ -146,8 +182,6 @@ proc writeConfigFile*(path: string, current: string,
146182 buf.add " name = " & quoteVal (pr.name) & " \n "
147183 buf.add " url = " & quoteVal (pr.url) & " \n "
148184 buf.add " key = " & quoteVal (pr.key) & " \n "
149- if pr.modelPrefix != " " :
150- buf.add " model_prefix = " & quoteVal (pr.modelPrefix) & " \n "
151185 if pr.family != " " :
152186 buf.add " family = " & quoteVal (pr.family) & " \n "
153187 buf.add " models = " & quoteVal (formatModels (pr.models)) & " \n "
@@ -185,15 +219,14 @@ proc buildProfile*(current: string, providers: seq[ProviderRec],
185219 if pr.name == name:
186220 if pr.url == " " or pr.key == " " or pr.models.len == 0 :
187221 return Profile ()
188- if model == " " :
189- model = pr.models[0 ]
190- if pr.findModel (model) < 0 :
222+ let idx = if model == " " : 0 else : pr.findModel (model)
223+ if idx < 0 :
191224 return Profile ()
192- var prof = Profile (name: pr.name & " . " & model, url: pr.url,
193- key: pr.key, modelPrefix : pr.modelPrefix ,
194- model: model)
225+ let fullModel = pr.models[idx]
226+ var prof = Profile (name: pr.name & " . " & fullModel, url : pr.url ,
227+ key: pr.key, model: fullModel )
195228 prof.family = resolveFamily (pr, prof)
196- let (_, ver, vrt) = knownGoodTags (pr.name, pr.modelPrefix & model )
229+ let (_, ver, vrt) = knownGoodTags (pr.name, fullModel )
197230 prof.version = ver
198231 prof.variant = vrt
199232 return prof
@@ -231,14 +264,14 @@ proc loadProfile*(wanted: string): Profile =
231264 if prov.url == " " : die & " provider '{ name} ': url not set in { path} " , ExitConfig
232265 if prov.key == " " : die & " provider '{ name} ': key not set in { path} " , ExitConfig
233266 if prov.models.len == 0 : die & " provider '{ name} ': models not set in { path} " , ExitConfig
234- if model == " " :
235- model = prov.models[0 ]
236- if prov.findModel (model) < 0 :
267+ let idx = if model == " " : 0 else : prov.findModel (model)
268+ if idx < 0 :
237269 die & " provider '{ name} ': model '{ model} ' not in models list ({ prov.models.join (\ " , \" )})" , ExitConfig
238- var prof = Profile (name: prov.name & " ." & model, url: prov.url, key: prov.key,
239- modelPrefix: prov.modelPrefix, model: model)
270+ let fullModel = prov.models[idx]
271+ var prof = Profile (name: prov.name & " ." & fullModel, url: prov.url,
272+ key: prov.key, model: fullModel)
240273 prof.family = resolveFamily (prov, prof)
241- let (_, ver, vrt) = knownGoodTags (prov.name, prov.modelPrefix & model )
274+ let (_, ver, vrt) = knownGoodTags (prov.name, fullModel )
242275 prof.version = ver
243276 prof.variant = vrt
244277 if wanted == " " and not experimentalEnabled and not isKnownGood (prof):
@@ -312,44 +345,8 @@ proc defaultNameFromUrl*(url: string): string =
312345 if labels.len >= 2 : labels[^ 2 ]
313346 else : labels[0 ]
314347
315- proc commonModelPrefix * (models: seq [string ]): string =
316- # # Path-style prefix (everything up to and including the last `/`)
317- # # shared by every entry. For a single model, returns its own path
318- # # prefix so providers like `openai/gpt-oss-120b` get split into a
319- # # `model_prefix` of `openai/` and a bare `gpt-oss-120b`. When no
320- # # entries share a slashed prefix, returns "".
321- if models.len == 0 : return " "
322- if models.len == 1 :
323- let slash = models[0 ].rfind ('/' )
324- return if slash < 0 : " " else : models[0 ][0 .. slash]
325- var prefix = models[0 ]
326- for m in models[1 .. ^ 1 ]:
327- var i = 0
328- while i < prefix.len and i < m.len and prefix[i] == m[i]:
329- inc i
330- prefix = prefix[0 ..< i]
331- if prefix.len == 0 : return " "
332- let slash = prefix.rfind ('/' )
333- if slash < 0 : " " else : prefix[0 .. slash]
334-
335- proc shortModel * (model: string ): string =
336- # # The portion of a model id after the last `/`. Used in `:model`
337- # # listings and other places where the path-style provider namespace
338- # # is redundant. `gpt-oss-120b` for `openai/gpt-oss-120b`,
339- # # `glm-5p1` for `accounts/fireworks/models/glm-5p1`.
340- let slash = model.rfind ('/' )
341- if slash < 0 : model else : model[slash + 1 .. ^ 1 ]
342-
343- proc curatedFor * (provider: string ): (string , seq [string ]) =
344- # # Returns (commonPrefix, modelsWithoutPrefix) from KnownGoodCombos
345- # # for the given provider name.
346- var fullIds: seq [string ]
348+ proc curatedFor * (provider: string ): seq [string ] =
349+ # # Full model ids from KnownGoodCombos for the given provider name.
347350 let p = provider.toLowerAscii
348351 for c in KnownGoodCombos :
349- if c[0 ].toLowerAscii == p: fullIds.add c[1 ]
350- let prefix = commonModelPrefix (fullIds)
351- var stripped: seq [string ]
352- for m in fullIds:
353- if prefix != " " and m.startsWith (prefix): stripped.add m[prefix.len .. ^ 1 ]
354- else : stripped.add m
355- (prefix, stripped)
352+ if c[0 ].toLowerAscii == p: result .add c[1 ]
0 commit comments