@tryinget/pi-model-selection 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +78 -0
- package/README.md +31 -0
- package/index.d.ts +58 -0
- package/index.js +9 -0
- package/package.json +63 -0
- package/src/modelSelection.js +237 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
MIT License (with OpenAI/Anthropic/xAI/PRC Frontier Labs Rider)
|
|
2
|
+
|
|
3
|
+
Copyright (c) contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
ADDITIONAL RIDER / RESTRICTION (OpenAI / Anthropic / xAI / PRC Frontier Labs):
|
|
13
|
+
|
|
14
|
+
This rider is part of the "conditions" of this License. In the event of any
|
|
15
|
+
conflict between this rider and any other portion of this License, this rider
|
|
16
|
+
controls.
|
|
17
|
+
|
|
18
|
+
"Restricted Parties" means OpenAI, L.L.C.; Anthropic, PBC; xAI Corp.; and the
|
|
19
|
+
following PRC frontier AI labs and related entities: Baidu (including ERNIE-
|
|
20
|
+
related entities), Alibaba and Alibaba Cloud (including Qwen-related entities),
|
|
21
|
+
Tencent (including Hunyuan-related entities), ByteDance (including Doubao/Seed-
|
|
22
|
+
related entities), DeepSeek, Zhipu AI, Moonshot AI, MiniMax, 01.AI
|
|
23
|
+
(Lingyiwanwu), and iFlyTek. "Restricted Parties" also includes any of their
|
|
24
|
+
respective Affiliates and any person or entity acting directly or indirectly
|
|
25
|
+
on behalf of, for the benefit of, or under the direction of any of the
|
|
26
|
+
foregoing (including any officer, director, employee, contractor, agent,
|
|
27
|
+
consultant, service provider, or representative).
|
|
28
|
+
|
|
29
|
+
Notwithstanding any other provision of this License, no rights are granted to
|
|
30
|
+
any Restricted Party. Any purported license, sublicense, assignment, transfer,
|
|
31
|
+
or other permission to any Restricted Party is null and void absent the
|
|
32
|
+
express prior written permission of the copyright holders.
|
|
33
|
+
|
|
34
|
+
You may not provide, disclose, distribute, sublicense, sell, lease, lend,
|
|
35
|
+
host, make available, or otherwise permit access to the Software or any
|
|
36
|
+
derivative work of the Software (as defined in applicable copyright law)
|
|
37
|
+
(collectively, "Derivative Works") to or for any Restricted Party.
|
|
38
|
+
|
|
39
|
+
For purposes of this rider, "use" includes, without limitation: copying,
|
|
40
|
+
modifying, merging, publishing, distributing, sublicensing, selling,
|
|
41
|
+
transferring, making available, hosting, deploying, executing, benchmarking,
|
|
42
|
+
testing, analyzing, indexing, or incorporating the Software or any Derivative
|
|
43
|
+
Works into any dataset, training corpus, evaluation harness, or pipeline for
|
|
44
|
+
machine learning or other automated systems.
|
|
45
|
+
|
|
46
|
+
This rider applies to the Software and all Derivative Works. As a condition of
|
|
47
|
+
use, you agree that this rider is a precondition to exercising any rights
|
|
48
|
+
under this License, and you agree that any distribution of the Software or any
|
|
49
|
+
Derivative Works must include this rider provision unmodified.
|
|
50
|
+
|
|
51
|
+
Any breach of this rider automatically and immediately terminates the
|
|
52
|
+
permissions granted by this License. Upon termination, you must immediately
|
|
53
|
+
cease all use and distribution of the Software and any Derivative Works and
|
|
54
|
+
destroy all copies under your control.
|
|
55
|
+
|
|
56
|
+
You agree that a breach of this rider would cause irreparable harm and that
|
|
57
|
+
the copyright holders may seek injunctive or other equitable relief to enforce
|
|
58
|
+
this rider, in addition to any other remedies available at law. To the maximum
|
|
59
|
+
extent permitted by applicable law, the prevailing party in any action to
|
|
60
|
+
enforce this rider shall be entitled to recover reasonable attorneys' fees and
|
|
61
|
+
costs.
|
|
62
|
+
|
|
63
|
+
For purposes of this rider, "Affiliate" means any entity that directly or
|
|
64
|
+
indirectly controls, is controlled by, or is under common control with the
|
|
65
|
+
specified party. "Control" means ownership of more than 50% of the voting
|
|
66
|
+
securities or other ownership interest, or the power to direct management or
|
|
67
|
+
policies by contract or otherwise.
|
|
68
|
+
|
|
69
|
+
The above copyright notice and this permission notice shall be included in all
|
|
70
|
+
copies or substantial portions of the Software.
|
|
71
|
+
|
|
72
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
73
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
74
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
75
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
76
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
77
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
78
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
summary: "@tryinget/pi-model-selection package overview."
|
|
3
|
+
read_when:
|
|
4
|
+
- "Orienting to this package or directory before changing its behavior."
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# @tryinget/pi-model-selection
|
|
8
|
+
|
|
9
|
+
Shared Pi model-selection and auth-resolution primitives.
|
|
10
|
+
|
|
11
|
+
This is a top-level support package at `packages/pi-model-selection`, not a `pi-interaction` subpackage. Consumers should depend on `@tryinget/pi-model-selection` directly rather than depending on `@tryinget/pi-interaction` for model-selection helpers.
|
|
12
|
+
|
|
13
|
+
This package owns prompt-template-model-compatible model resolver semantics that are needed by more than one extension:
|
|
14
|
+
|
|
15
|
+
- exact `modelId`
|
|
16
|
+
- exact `provider/modelId`
|
|
17
|
+
- comma-separated fallback lists
|
|
18
|
+
- current-model preservation when the active model matches any candidate
|
|
19
|
+
- provider-priority ordering for ambiguous bare model IDs:
|
|
20
|
+
1. `openai-codex`
|
|
21
|
+
2. `anthropic`
|
|
22
|
+
3. `github-copilot`
|
|
23
|
+
4. `openrouter`
|
|
24
|
+
- auth-aware selection via `getAvailable()`, `isUsingOAuth()`, `getApiKey()`, and `getApiKeyAndHeaders()`
|
|
25
|
+
- preservation of model-level `headers`
|
|
26
|
+
|
|
27
|
+
It intentionally does not own prompt-template slash-command registration, compaction preset parsing, loops/workflows, or subagent execution.
|
|
28
|
+
|
|
29
|
+
## Template baseline
|
|
30
|
+
|
|
31
|
+
This package was reconciled through `../pi-extensions-template` in `simple-package` mode, then intentionally adapted as a root-managed support library. It keeps template lineage and baseline files such as `.copier-answers.yml`, `biome.jsonc`, `policy/stack-lane.json`, and `docs/tech-stack.local.md`, but it deliberately omits `package.json#pi.extensions` and `package.json#pi.prompts` so it cannot register slash commands or package prompts by accident.
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export interface PiModelLike {
|
|
2
|
+
id?: string;
|
|
3
|
+
provider?: string;
|
|
4
|
+
headers?: Record<string, unknown>;
|
|
5
|
+
[key: string]: unknown;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface PiModelRegistryLike {
|
|
9
|
+
find?(provider: string, modelId: string): PiModelLike | undefined;
|
|
10
|
+
getAll?(): PiModelLike[];
|
|
11
|
+
getAvailable?(): PiModelLike[];
|
|
12
|
+
isUsingOAuth?(model: PiModelLike): boolean;
|
|
13
|
+
getApiKey?(model: PiModelLike): Promise<string | undefined> | string | undefined;
|
|
14
|
+
getApiKeyAndHeaders?(
|
|
15
|
+
model: PiModelLike,
|
|
16
|
+
):
|
|
17
|
+
| Promise<{ ok?: boolean; apiKey?: string; headers?: Record<string, unknown>; error?: string }>
|
|
18
|
+
| { ok?: boolean; apiKey?: string; headers?: Record<string, unknown>; error?: string };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface PiModelSelectionContext {
|
|
22
|
+
model?: PiModelLike;
|
|
23
|
+
modelRegistry?: PiModelRegistryLike;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface SelectedModelCandidate {
|
|
27
|
+
model: PiModelLike;
|
|
28
|
+
alreadyActive: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ResolvedModelAuth {
|
|
32
|
+
ok: boolean;
|
|
33
|
+
apiKey?: string;
|
|
34
|
+
headers?: Record<string, unknown>;
|
|
35
|
+
error?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const PREFERRED_PROVIDERS: string[];
|
|
39
|
+
|
|
40
|
+
export function parseProviderModel(value: unknown): { provider: string; modelId: string };
|
|
41
|
+
export function parseModelSpecList(value: unknown): string[];
|
|
42
|
+
export function resolveModelAuth(
|
|
43
|
+
ctx: PiModelSelectionContext,
|
|
44
|
+
model: PiModelLike,
|
|
45
|
+
): Promise<ResolvedModelAuth>;
|
|
46
|
+
export function resolveModelReference(ctx: PiModelSelectionContext, reference: string): PiModelLike;
|
|
47
|
+
export function selectModelCandidate(
|
|
48
|
+
modelSpecs: string[] | string,
|
|
49
|
+
currentModel: PiModelLike | undefined,
|
|
50
|
+
ctx: PiModelSelectionContext,
|
|
51
|
+
): Promise<SelectedModelCandidate | undefined>;
|
|
52
|
+
|
|
53
|
+
export const modelSelectionInternals: {
|
|
54
|
+
getModelCandidates(modelSpec: string, registry?: PiModelRegistryLike): PiModelLike[];
|
|
55
|
+
modelDisplayRef(model: PiModelLike): string;
|
|
56
|
+
modelSpecMatches(modelSpec: string, model: PiModelLike): boolean;
|
|
57
|
+
orderMatchesByProviderPreference(models: PiModelLike[]): PiModelLike[];
|
|
58
|
+
};
|
package/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tryinget/pi-model-selection",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared Pi model selection and auth resolution primitives.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.js",
|
|
7
|
+
"types": "./index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./index.d.ts",
|
|
11
|
+
"import": "./index.js",
|
|
12
|
+
"default": "./index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/tryingET/pi-extensions.git",
|
|
19
|
+
"directory": "packages/pi-model-selection"
|
|
20
|
+
},
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/tryingET/pi-extensions/issues"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/tryingET/pi-extensions/tree/main/packages/pi-model-selection",
|
|
25
|
+
"keywords": [
|
|
26
|
+
"pi-package",
|
|
27
|
+
"model-selection",
|
|
28
|
+
"prompt-template"
|
|
29
|
+
],
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"registry": "https://registry.npmjs.org/",
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=22"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"fix": "bash ../../scripts/package-quality-gate.sh fix . --mode simple-package",
|
|
39
|
+
"lint": "bash ../../scripts/package-quality-gate.sh lint . --mode simple-package",
|
|
40
|
+
"typecheck": "bash ../../scripts/package-quality-gate.sh typecheck . --mode simple-package",
|
|
41
|
+
"test": "bash ../../scripts/package-quality-gate.sh test . --mode simple-package",
|
|
42
|
+
"quality:pre-commit": "bash ../../scripts/package-quality-gate.sh pre-commit . --mode simple-package",
|
|
43
|
+
"quality:pre-push": "bash ../../scripts/package-quality-gate.sh pre-push . --mode simple-package",
|
|
44
|
+
"quality:ci": "bash ../../scripts/package-quality-gate.sh ci . --mode simple-package",
|
|
45
|
+
"check": "npm run quality:ci",
|
|
46
|
+
"release:check": "npm pack --dry-run --json",
|
|
47
|
+
"release:check:quick": "npm pack --dry-run --json"
|
|
48
|
+
},
|
|
49
|
+
"files": [
|
|
50
|
+
"index.js",
|
|
51
|
+
"index.d.ts",
|
|
52
|
+
"src"
|
|
53
|
+
],
|
|
54
|
+
"x-pi-template": {
|
|
55
|
+
"scaffoldMode": "simple-package",
|
|
56
|
+
"workspacePath": "packages/pi-model-selection",
|
|
57
|
+
"releaseComponent": "pi-model-selection",
|
|
58
|
+
"releaseConfigMode": "component"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@biomejs/biome": "2.4.4"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
export const PREFERRED_PROVIDERS = ["openai-codex", "anthropic", "github-copilot", "openrouter"];
|
|
2
|
+
|
|
3
|
+
function normalizeText(value) {
|
|
4
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function getModelRegistryModels(modelRegistry) {
|
|
8
|
+
if (typeof modelRegistry?.getAll !== "function") return [];
|
|
9
|
+
const models = modelRegistry.getAll();
|
|
10
|
+
return Array.isArray(models) ? models.filter(Boolean) : [];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getAvailableRegistryModels(modelRegistry) {
|
|
14
|
+
if (typeof modelRegistry?.getAvailable !== "function") return undefined;
|
|
15
|
+
const models = modelRegistry.getAvailable();
|
|
16
|
+
return Array.isArray(models) ? models.filter(Boolean) : undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function modelProvider(model) {
|
|
20
|
+
return normalizeText(model?.provider);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function modelId(model) {
|
|
24
|
+
return normalizeText(model?.id);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function modelDisplayRef(model) {
|
|
28
|
+
const provider = modelProvider(model);
|
|
29
|
+
const id = modelId(model);
|
|
30
|
+
if (provider && id) return `${provider}/${id}`;
|
|
31
|
+
return id ?? provider ?? "<unknown-model>";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function sameModel(a, b) {
|
|
35
|
+
return Boolean(a && b && modelProvider(a) === modelProvider(b) && modelId(a) === modelId(b));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function modelSpecMatches(modelSpec, model) {
|
|
39
|
+
const spec = normalizeText(modelSpec);
|
|
40
|
+
if (!spec || !model) return false;
|
|
41
|
+
|
|
42
|
+
const slashIndex = spec.indexOf("/");
|
|
43
|
+
if (slashIndex !== -1) {
|
|
44
|
+
const provider = spec.slice(0, slashIndex);
|
|
45
|
+
const id = spec.slice(slashIndex + 1);
|
|
46
|
+
return provider === modelProvider(model) && id === modelId(model);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return spec === modelId(model);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function orderMatchesByProviderPreference(models) {
|
|
53
|
+
const prioritized = [];
|
|
54
|
+
const seen = new Set();
|
|
55
|
+
|
|
56
|
+
for (const provider of PREFERRED_PROVIDERS) {
|
|
57
|
+
for (const model of models) {
|
|
58
|
+
const key = modelDisplayRef(model);
|
|
59
|
+
if (modelProvider(model) === provider && !seen.has(key)) {
|
|
60
|
+
prioritized.push(model);
|
|
61
|
+
seen.add(key);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (const model of models) {
|
|
67
|
+
const key = modelDisplayRef(model);
|
|
68
|
+
if (!seen.has(key)) {
|
|
69
|
+
prioritized.push(model);
|
|
70
|
+
seen.add(key);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return prioritized;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isValidModelSelectionSpec(spec) {
|
|
78
|
+
if (!spec || spec.includes("*") || /\s/.test(spec)) return false;
|
|
79
|
+
|
|
80
|
+
const segments = spec.split("/");
|
|
81
|
+
if (segments.length === 1) return true;
|
|
82
|
+
if (segments.length !== 2) return false;
|
|
83
|
+
return segments[0].length > 0 && segments[1].length > 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function parseProviderModel(value) {
|
|
87
|
+
const raw = normalizeText(value);
|
|
88
|
+
if (!raw) {
|
|
89
|
+
throw new Error("Invalid model reference: expected provider/modelId");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const separatorIndex = raw.indexOf("/");
|
|
93
|
+
if (separatorIndex <= 0 || separatorIndex === raw.length - 1) {
|
|
94
|
+
throw new Error(`Invalid model reference '${raw}'. Expected provider/modelId`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const provider = raw.slice(0, separatorIndex).trim();
|
|
98
|
+
const modelId = raw.slice(separatorIndex + 1).trim();
|
|
99
|
+
if (!provider || !modelId || modelId.includes("/")) {
|
|
100
|
+
throw new Error(`Invalid model reference '${raw}'. Expected provider/modelId`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { provider, modelId };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function parseModelSpecList(value) {
|
|
107
|
+
const raw = normalizeText(value);
|
|
108
|
+
if (!raw) {
|
|
109
|
+
throw new Error("Model spec list must be a non-empty string");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const specs = raw
|
|
113
|
+
.split(",")
|
|
114
|
+
.map((item) => item.trim())
|
|
115
|
+
.filter(Boolean);
|
|
116
|
+
|
|
117
|
+
if (specs.length === 0) {
|
|
118
|
+
throw new Error("Model spec list must contain at least one model");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const invalidSpec = specs.find((spec) => !isValidModelSelectionSpec(spec));
|
|
122
|
+
if (invalidSpec) {
|
|
123
|
+
throw new Error(`Invalid model spec '${invalidSpec}'. Expected modelId or provider/modelId`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return specs;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function getModelCandidates(modelSpec, registry) {
|
|
130
|
+
const spec = normalizeText(modelSpec);
|
|
131
|
+
if (!spec) return [];
|
|
132
|
+
|
|
133
|
+
const slashIndex = spec.indexOf("/");
|
|
134
|
+
if (slashIndex !== -1) {
|
|
135
|
+
let parsed;
|
|
136
|
+
try {
|
|
137
|
+
parsed = parseProviderModel(spec);
|
|
138
|
+
} catch {
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (typeof registry?.find === "function") {
|
|
143
|
+
const found = registry.find(parsed.provider, parsed.modelId);
|
|
144
|
+
return found ? [found] : [];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return getModelRegistryModels(registry).filter(
|
|
148
|
+
(model) => modelProvider(model) === parsed.provider && modelId(model) === parsed.modelId,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const allMatches = getModelRegistryModels(registry).filter((model) => modelId(model) === spec);
|
|
153
|
+
if (allMatches.length <= 1) return allMatches;
|
|
154
|
+
return orderMatchesByProviderPreference(allMatches);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function resolveModelAuth(ctx, model) {
|
|
158
|
+
const registry = ctx?.modelRegistry;
|
|
159
|
+
|
|
160
|
+
if (typeof registry?.getApiKeyAndHeaders === "function") {
|
|
161
|
+
const result = await registry.getApiKeyAndHeaders(model);
|
|
162
|
+
if (result?.ok === false) return result;
|
|
163
|
+
return {
|
|
164
|
+
ok: true,
|
|
165
|
+
apiKey: result?.apiKey,
|
|
166
|
+
headers: result?.headers ?? model?.headers,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (typeof registry?.getApiKey === "function") {
|
|
171
|
+
const apiKey = await registry.getApiKey(model);
|
|
172
|
+
return {
|
|
173
|
+
ok: true,
|
|
174
|
+
apiKey,
|
|
175
|
+
headers: model?.headers,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
ok: false,
|
|
181
|
+
error: "Model registry does not expose getApiKeyAndHeaders or getApiKey",
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function hasUsableAuth(model, ctx) {
|
|
186
|
+
const registry = ctx?.modelRegistry;
|
|
187
|
+
const availableModels = getAvailableRegistryModels(registry);
|
|
188
|
+
if (availableModels?.some((candidate) => sameModel(candidate, model))) return true;
|
|
189
|
+
|
|
190
|
+
if (typeof registry?.isUsingOAuth === "function" && !registry.isUsingOAuth(model)) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const auth = await resolveModelAuth(ctx, model);
|
|
195
|
+
return auth.ok && Boolean(auth.apiKey || auth.headers);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function selectModelCandidate(modelSpecs, currentModel, ctx) {
|
|
199
|
+
const specs = Array.isArray(modelSpecs) ? modelSpecs : parseModelSpecList(modelSpecs);
|
|
200
|
+
|
|
201
|
+
if (currentModel && specs.some((spec) => modelSpecMatches(spec, currentModel))) {
|
|
202
|
+
return { model: currentModel, alreadyActive: true };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
for (const spec of specs) {
|
|
206
|
+
for (const model of getModelCandidates(spec, ctx?.modelRegistry)) {
|
|
207
|
+
if (await hasUsableAuth(model, ctx)) {
|
|
208
|
+
return { model, alreadyActive: false };
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function resolveModelReference(ctx, reference) {
|
|
217
|
+
const specs = parseModelSpecList(reference);
|
|
218
|
+
if (specs.length !== 1) {
|
|
219
|
+
throw new Error(`Model reference '${reference}' contains multiple fallback specs`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const candidates = getModelCandidates(specs[0], ctx?.modelRegistry);
|
|
223
|
+
if (candidates.length === 1) return candidates[0];
|
|
224
|
+
if (candidates.length > 1) {
|
|
225
|
+
throw new Error(
|
|
226
|
+
`Model reference '${reference}' is ambiguous: ${candidates.map(modelDisplayRef).join(", ")}`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
throw new Error(`Model reference '${reference}' did not match any registered model`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export const modelSelectionInternals = {
|
|
233
|
+
getModelCandidates,
|
|
234
|
+
modelDisplayRef,
|
|
235
|
+
modelSpecMatches,
|
|
236
|
+
orderMatchesByProviderPreference,
|
|
237
|
+
};
|