(modelId: string)
| 197 | return out; |
| 198 | } |
| 199 | |
| 200 | resolveModel(modelId: string): { provider: Provider; modelInfo: ModelInfo; resolvedId: string } | null { |
| 201 | // Try exact match first |
| 202 | const direct = this.modelIndex.get(modelId); |
| 203 | if (direct) { |
| 204 | // The matched model's OWN id is exactly what the provider expects on the wire. |
| 205 | // Using it (instead of stripping a leading "provider/") is correct in every case: |
| 206 | // - `openai/gpt-4o` → info.id `gpt-4o` (provider prefix dropped) |
| 207 | // - `qwen/qwen3-coder-next`→ info.id `qwen/qwen3-coder-next` (HF publisher kept) |
| 208 | // - `nvidia/nemotron-3-super-120b-a12b` on a provider NAMED nvidia → info.id kept VERBATIM. |
| 209 | // The old "strip if first segment is a provider name" heuristic broke that last case: a |
| 210 | // custom provider named `nvidia` made it strip the model's `nvidia/` VENDOR prefix, so the |
| 211 | // gateway got `nemotron-3-...` and 404'd. info.id never has that collision. |
| 212 | return { provider: direct.provider, modelInfo: direct.info, resolvedId: direct.info.id }; |
| 213 | } |
| 214 | // Try fuzzy match — only for QodeX-provider prefixes, same rule as above. |
| 215 | for (const [key, val] of this.modelIndex) { |
| 216 | if (key.includes('/') && key.endsWith('/' + modelId)) { |
| 217 | const prefix = key.slice(0, key.length - modelId.length - 1); |
| 218 | if (this.providers.has(prefix)) { |
| 219 | return { provider: val.provider, modelInfo: val.info, resolvedId: modelId }; |
| 220 | } |
| 221 | } |
| 222 | } |
| 223 | |
| 224 | // Partial match (case-insensitive): lets `--model qwen2.5` select |
| 225 | // `qwen2.5-coder:32b` when that's the ONLY model matching. Prefix matches are |
| 226 | // preferred over substring; an ambiguous query (matching 2+ distinct models) |
| 227 | // resolves to nothing so we never silently pick the wrong model — route() |
| 228 | // turns that into an error listing the candidates. |
| 229 | const matches = this.matchModelCandidates(modelId); |
| 230 | if (matches.length === 1) { |
| 231 | const val = matches[0]!; |
| 232 | return { provider: val.provider, modelInfo: val.info, resolvedId: val.info.id }; |
| 233 | } |
| 234 | |
| 235 | return null; |
| 236 | } |
| 237 | |
| 238 | /** |
no test coverage detected