jeopi-catalog 16.2.13
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/CHANGELOG.md +523 -0
- package/README.md +43 -0
- package/dist/types/build.d.ts +3 -0
- package/dist/types/compat/anthropic.d.ts +11 -0
- package/dist/types/compat/apply.d.ts +7 -0
- package/dist/types/compat/devin.d.ts +9 -0
- package/dist/types/compat/openai.d.ts +28 -0
- package/dist/types/discovery/antigravity.d.ts +70 -0
- package/dist/types/discovery/codex.d.ts +38 -0
- package/dist/types/discovery/cursor-gen/agent_pb.d.ts +13022 -0
- package/dist/types/discovery/cursor.d.ts +23 -0
- package/dist/types/discovery/devin-gen/buf/validate/validate_pb.d.ts +1715 -0
- package/dist/types/discovery/devin-gen/exa/analytics_pb/analytics_pb.d.ts +693 -0
- package/dist/types/discovery/devin-gen/exa/api_server_pb/api_server_pb.d.ts +9158 -0
- package/dist/types/discovery/devin-gen/exa/auth_pb/auth_pb.d.ts +52 -0
- package/dist/types/discovery/devin-gen/exa/auto_cascade_common_pb/auto_cascade_common_pb.d.ts +276 -0
- package/dist/types/discovery/devin-gen/exa/bug_checker_pb/bug_checker_pb.d.ts +78 -0
- package/dist/types/discovery/devin-gen/exa/cascade_plugins_pb/cascade_plugins_pb.d.ts +999 -0
- package/dist/types/discovery/devin-gen/exa/chat_pb/chat_pb.d.ts +1704 -0
- package/dist/types/discovery/devin-gen/exa/code_edit/code_edit_pb/code_edit_pb.d.ts +656 -0
- package/dist/types/discovery/devin-gen/exa/codeium_common_pb/codeium_common_pb.d.ts +15014 -0
- package/dist/types/discovery/devin-gen/exa/context_module_pb/context_module_pb.d.ts +607 -0
- package/dist/types/discovery/devin-gen/exa/cortex_pb/cortex_pb.d.ts +11830 -0
- package/dist/types/discovery/devin-gen/exa/diff_action_pb/diff_action_pb.d.ts +248 -0
- package/dist/types/discovery/devin-gen/exa/index_pb/index_pb.d.ts +1747 -0
- package/dist/types/discovery/devin-gen/exa/knowledge_base_pb/knowledge_base_pb.d.ts +509 -0
- package/dist/types/discovery/devin-gen/exa/language_server_pb/language_server_pb.d.ts +9048 -0
- package/dist/types/discovery/devin-gen/exa/opensearch_clients_pb/opensearch_clients_pb.d.ts +1760 -0
- package/dist/types/discovery/devin-gen/exa/prompt_pb/prompt_pb.d.ts +286 -0
- package/dist/types/discovery/devin-gen/exa/reactive_component_pb/reactive_component_pb.d.ts +405 -0
- package/dist/types/discovery/devin-gen/exa/trust_pb/trust_pb.d.ts +582 -0
- package/dist/types/discovery/devin.d.ts +24 -0
- package/dist/types/discovery/gemini.d.ts +25 -0
- package/dist/types/discovery/gitlab-duo-workflow.d.ts +30 -0
- package/dist/types/discovery/index.d.ts +5 -0
- package/dist/types/discovery/openai-compatible.d.ts +74 -0
- package/dist/types/effort.d.ts +9 -0
- package/dist/types/fireworks-model-id.d.ts +23 -0
- package/dist/types/hosts.d.ts +137 -0
- package/dist/types/identity/bundled.d.ts +3 -0
- package/dist/types/identity/classify.d.ts +69 -0
- package/dist/types/identity/dialect.d.ts +3 -0
- package/dist/types/identity/family.d.ts +139 -0
- package/dist/types/identity/id.d.ts +12 -0
- package/dist/types/identity/index.d.ts +8 -0
- package/dist/types/identity/markers.d.ts +4 -0
- package/dist/types/identity/priority.d.ts +1 -0
- package/dist/types/identity/reference.d.ts +23 -0
- package/dist/types/index.d.ts +17 -0
- package/dist/types/model-cache.d.ts +17 -0
- package/dist/types/model-manager.d.ts +68 -0
- package/dist/types/model-thinking.d.ts +79 -0
- package/dist/types/models.d.ts +12 -0
- package/dist/types/provider-models/bundled-references.d.ts +11 -0
- package/dist/types/provider-models/descriptor-types.d.ts +74 -0
- package/dist/types/provider-models/descriptors.d.ts +427 -0
- package/dist/types/provider-models/google.d.ts +27 -0
- package/dist/types/provider-models/index.d.ts +6 -0
- package/dist/types/provider-models/ollama.d.ts +9 -0
- package/dist/types/provider-models/openai-compat.d.ts +450 -0
- package/dist/types/provider-models/special.d.ts +33 -0
- package/dist/types/types.d.ts +656 -0
- package/dist/types/utils.d.ts +20 -0
- package/dist/types/variant-collapse.d.ts +133 -0
- package/dist/types/wire/codex.d.ts +26 -0
- package/dist/types/wire/coreweave.d.ts +12 -0
- package/dist/types/wire/gemini-headers.d.ts +37 -0
- package/dist/types/wire/github-copilot.d.ts +45 -0
- package/package.json +101 -0
- package/src/build.ts +47 -0
- package/src/compat/anthropic.ts +93 -0
- package/src/compat/apply.ts +15 -0
- package/src/compat/devin.ts +12 -0
- package/src/compat/openai.ts +689 -0
- package/src/discovery/antigravity.ts +253 -0
- package/src/discovery/codex.ts +371 -0
- package/src/discovery/cursor-gen/agent_pb.ts +15274 -0
- package/src/discovery/cursor.ts +314 -0
- package/src/discovery/devin-gen/buf/validate/validate_pb.ts +1862 -0
- package/src/discovery/devin-gen/exa/analytics_pb/analytics_pb.ts +871 -0
- package/src/discovery/devin-gen/exa/api_server_pb/api_server_pb.ts +11083 -0
- package/src/discovery/devin-gen/exa/auth_pb/auth_pb.ts +71 -0
- package/src/discovery/devin-gen/exa/auto_cascade_common_pb/auto_cascade_common_pb.ts +348 -0
- package/src/discovery/devin-gen/exa/bug_checker_pb/bug_checker_pb.ts +103 -0
- package/src/discovery/devin-gen/exa/cascade_plugins_pb/cascade_plugins_pb.ts +1198 -0
- package/src/discovery/devin-gen/exa/chat_pb/chat_pb.ts +2063 -0
- package/src/discovery/devin-gen/exa/code_edit/code_edit_pb/code_edit_pb.ts +810 -0
- package/src/discovery/devin-gen/exa/codeium_common_pb/codeium_common_pb.ts +18354 -0
- package/src/discovery/devin-gen/exa/context_module_pb/context_module_pb.ts +732 -0
- package/src/discovery/devin-gen/exa/cortex_pb/cortex_pb.ts +14277 -0
- package/src/discovery/devin-gen/exa/diff_action_pb/diff_action_pb.ts +312 -0
- package/src/discovery/devin-gen/exa/index_pb/index_pb.ts +2106 -0
- package/src/discovery/devin-gen/exa/knowledge_base_pb/knowledge_base_pb.ts +623 -0
- package/src/discovery/devin-gen/exa/language_server_pb/language_server_pb.ts +10918 -0
- package/src/discovery/devin-gen/exa/opensearch_clients_pb/opensearch_clients_pb.ts +2125 -0
- package/src/discovery/devin-gen/exa/prompt_pb/prompt_pb.ts +361 -0
- package/src/discovery/devin-gen/exa/reactive_component_pb/reactive_component_pb.ts +450 -0
- package/src/discovery/devin-gen/exa/trust_pb/trust_pb.ts +712 -0
- package/src/discovery/devin.ts +151 -0
- package/src/discovery/gemini.ts +260 -0
- package/src/discovery/gitlab-duo-workflow.ts +855 -0
- package/src/discovery/index.ts +5 -0
- package/src/discovery/openai-compatible.ts +249 -0
- package/src/effort.ts +16 -0
- package/src/fireworks-model-id.ts +50 -0
- package/src/hosts.ts +129 -0
- package/src/identity/bundled.ts +28 -0
- package/src/identity/classify.ts +214 -0
- package/src/identity/dialect.ts +42 -0
- package/src/identity/family.ts +296 -0
- package/src/identity/id.ts +81 -0
- package/src/identity/index.ts +8 -0
- package/src/identity/markers.ts +49 -0
- package/src/identity/priority.ts +57 -0
- package/src/identity/reference.ts +148 -0
- package/src/index.ts +17 -0
- package/src/model-cache.ts +171 -0
- package/src/model-manager.ts +520 -0
- package/src/model-thinking.ts +760 -0
- package/src/models.json +88898 -0
- package/src/models.json.d.ts +9 -0
- package/src/models.ts +64 -0
- package/src/provider-models/bundled-references.ts +58 -0
- package/src/provider-models/descriptor-types.ts +79 -0
- package/src/provider-models/descriptors.ts +503 -0
- package/src/provider-models/google.ts +107 -0
- package/src/provider-models/index.ts +6 -0
- package/src/provider-models/ollama.ts +172 -0
- package/src/provider-models/openai-compat.ts +4283 -0
- package/src/provider-models/special.ts +159 -0
- package/src/types.ts +786 -0
- package/src/utils.ts +69 -0
- package/src/variant-collapse.ts +1065 -0
- package/src/wire/codex.ts +43 -0
- package/src/wire/coreweave.ts +51 -0
- package/src/wire/gemini-headers.ts +74 -0
- package/src/wire/github-copilot.ts +121 -0
|
@@ -0,0 +1,4283 @@
|
|
|
1
|
+
import {
|
|
2
|
+
fetchOpenAICompatibleModels,
|
|
3
|
+
type OpenAICompatibleModelMapperContext,
|
|
4
|
+
type OpenAICompatibleModelRecord,
|
|
5
|
+
} from "../discovery/openai-compatible";
|
|
6
|
+
import { Effort } from "../effort";
|
|
7
|
+
import { FIREWORKS_FAST_SUFFIX, toFireworksPublicModelId } from "../fireworks-model-id";
|
|
8
|
+
import { isGlmVisionModelId, isGrokReasoningEffortCapable, isReasoningGlmModelId } from "../identity/family";
|
|
9
|
+
import type { ModelManagerOptions } from "../model-manager";
|
|
10
|
+
import { getBundledModels } from "../models";
|
|
11
|
+
import type { Api, FetchImpl, Model, ModelSpec, OpenAICompat, Provider, ThinkingConfig } from "../types";
|
|
12
|
+
import { discoveryFetch, isAnthropicOAuthToken, isRecord, toBoolean, toNumber, toPositiveNumber } from "../utils";
|
|
13
|
+
import { coreWeaveProjectHeaders } from "../wire/coreweave";
|
|
14
|
+
import {
|
|
15
|
+
COPILOT_API_HEADERS,
|
|
16
|
+
getGitHubCopilotBaseUrl,
|
|
17
|
+
isPersonalGitHubCopilotBaseUrl,
|
|
18
|
+
parseGitHubCopilotApiKey,
|
|
19
|
+
} from "../wire/github-copilot";
|
|
20
|
+
import { createBundledReferenceMap, createReferenceResolver, toModelSpec } from "./bundled-references";
|
|
21
|
+
|
|
22
|
+
const MODELS_DEV_URL = "https://models.dev/api.json";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Uses a cancellable timer rather than the native abort-timeout helper so
|
|
26
|
+
* successful fast discovery requests do not leave armed timeout signals for
|
|
27
|
+
* concurrent GC to trip over later.
|
|
28
|
+
*/
|
|
29
|
+
async function withCatalogDiscoveryTimeout<T>(timeoutMs: number, run: (signal: AbortSignal) => Promise<T>): Promise<T> {
|
|
30
|
+
const controller = new AbortController();
|
|
31
|
+
const timer = setTimeout(
|
|
32
|
+
() => controller.abort(new DOMException("The operation timed out.", "TimeoutError")),
|
|
33
|
+
timeoutMs,
|
|
34
|
+
);
|
|
35
|
+
try {
|
|
36
|
+
return await run(controller.signal);
|
|
37
|
+
} finally {
|
|
38
|
+
clearTimeout(timer);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const ANTHROPIC_BASE_URL = "https://api.anthropic.com/v1";
|
|
43
|
+
const ANTHROPIC_OAUTH_BETA =
|
|
44
|
+
"claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,redact-thinking-2026-02-12,context-management-2025-06-27,prompt-caching-scope-2026-01-05,mid-conversation-system-2026-04-07,advanced-tool-use-2025-11-20,effort-2025-11-24,extended-cache-ttl-2025-04-11";
|
|
45
|
+
|
|
46
|
+
export interface ModelsDevModel {
|
|
47
|
+
id?: string;
|
|
48
|
+
name?: string;
|
|
49
|
+
tool_call?: boolean;
|
|
50
|
+
reasoning?: boolean;
|
|
51
|
+
limit?: {
|
|
52
|
+
context?: number;
|
|
53
|
+
output?: number;
|
|
54
|
+
};
|
|
55
|
+
cost?: {
|
|
56
|
+
input?: number;
|
|
57
|
+
output?: number;
|
|
58
|
+
cache_read?: number;
|
|
59
|
+
cache_write?: number;
|
|
60
|
+
};
|
|
61
|
+
modalities?: {
|
|
62
|
+
input?: string[];
|
|
63
|
+
};
|
|
64
|
+
status?: string;
|
|
65
|
+
provider?: { npm?: string };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function toModelName(value: unknown, fallback: string): string {
|
|
69
|
+
if (typeof value !== "string") {
|
|
70
|
+
return fallback;
|
|
71
|
+
}
|
|
72
|
+
const trimmed = value.trim();
|
|
73
|
+
return trimmed.length > 0 ? trimmed : fallback;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function toInputCapabilities(value: unknown): ("text" | "image")[] {
|
|
77
|
+
if (!Array.isArray(value)) {
|
|
78
|
+
return ["text"];
|
|
79
|
+
}
|
|
80
|
+
const supportsImage = value.some(item => item === "image");
|
|
81
|
+
return supportsImage ? ["text", "image"] : ["text"];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function fetchModelsDevPayload(fetchImpl: FetchImpl = discoveryFetch()): Promise<unknown> {
|
|
85
|
+
const response = await fetchImpl(MODELS_DEV_URL, {
|
|
86
|
+
method: "GET",
|
|
87
|
+
headers: { Accept: "application/json" },
|
|
88
|
+
});
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
throw new Error(`models.dev fetch failed: ${response.status}`);
|
|
91
|
+
}
|
|
92
|
+
return response.json();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function mapAnthropicModelsDev(payload: unknown, baseUrl: string): ModelSpec<"anthropic-messages">[] {
|
|
96
|
+
if (!isRecord(payload)) {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
const anthropicPayload = payload.anthropic;
|
|
100
|
+
if (!isRecord(anthropicPayload)) {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
const modelsValue = anthropicPayload.models;
|
|
104
|
+
if (!isRecord(modelsValue)) {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const models: ModelSpec<"anthropic-messages">[] = [];
|
|
109
|
+
for (const [modelId, rawModel] of Object.entries(modelsValue)) {
|
|
110
|
+
if (!isRecord(rawModel)) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const model = rawModel as ModelsDevModel;
|
|
114
|
+
if (model.tool_call !== true) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
models.push({
|
|
118
|
+
id: modelId,
|
|
119
|
+
name: toModelName(model.name, modelId),
|
|
120
|
+
api: "anthropic-messages",
|
|
121
|
+
provider: "anthropic",
|
|
122
|
+
baseUrl,
|
|
123
|
+
reasoning: model.reasoning === true,
|
|
124
|
+
input: toInputCapabilities(model.modalities?.input),
|
|
125
|
+
cost: {
|
|
126
|
+
input: toNumber(model.cost?.input) ?? 0,
|
|
127
|
+
output: toNumber(model.cost?.output) ?? 0,
|
|
128
|
+
cacheRead: toNumber(model.cost?.cache_read) ?? 0,
|
|
129
|
+
cacheWrite: toNumber(model.cost?.cache_write) ?? 0,
|
|
130
|
+
},
|
|
131
|
+
contextWindow: toPositiveNumber(model.limit?.context, null),
|
|
132
|
+
maxTokens: toPositiveNumber(model.limit?.output, null),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
models.sort((left, right) => left.id.localeCompare(right.id));
|
|
137
|
+
return models;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function buildAnthropicDiscoveryHeaders(apiKey: string): Record<string, string> {
|
|
141
|
+
const oauthToken = isAnthropicOAuthToken(apiKey);
|
|
142
|
+
const headers: Record<string, string> = {
|
|
143
|
+
"anthropic-version": "2023-06-01",
|
|
144
|
+
"anthropic-dangerous-direct-browser-access": "true",
|
|
145
|
+
"anthropic-beta": ANTHROPIC_OAUTH_BETA,
|
|
146
|
+
};
|
|
147
|
+
if (oauthToken) {
|
|
148
|
+
headers.Authorization = `Bearer ${apiKey}`;
|
|
149
|
+
} else {
|
|
150
|
+
headers["x-api-key"] = apiKey;
|
|
151
|
+
}
|
|
152
|
+
return headers;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function buildAnthropicReferenceMap(
|
|
156
|
+
modelsDevModels: readonly ModelSpec<"anthropic-messages">[],
|
|
157
|
+
): Map<string, ModelSpec<"anthropic-messages">> {
|
|
158
|
+
const merged = new Map<string, ModelSpec<"anthropic-messages">>();
|
|
159
|
+
for (const model of modelsDevModels) {
|
|
160
|
+
merged.set(model.id, model);
|
|
161
|
+
}
|
|
162
|
+
// Anthropic /v1/models does not carry token limits, so bundled metadata stays canonical
|
|
163
|
+
// for known models while models.dev only fills gaps for newly discovered ids.
|
|
164
|
+
const bundledModels = getBundledModels("anthropic").filter(
|
|
165
|
+
(model): model is Model<"anthropic-messages"> => model.api === "anthropic-messages",
|
|
166
|
+
);
|
|
167
|
+
for (const model of bundledModels) {
|
|
168
|
+
merged.set(model.id, toModelSpec(model));
|
|
169
|
+
}
|
|
170
|
+
return merged;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Curated Anthropic models that are live or limited-availability on the
|
|
175
|
+
* first-party `/v1/models` endpoint but that models.dev has not catalogued yet.
|
|
176
|
+
* Seeded into model generation so the bundled catalog is never gated on
|
|
177
|
+
* models.dev's update cadence; deduped behind upstream catalog / models.dev
|
|
178
|
+
* entries once those appear. Token limits and pricing are pinned either directly or
|
|
179
|
+
* in `applyAnthropicCatalogPolicy`, and `thinking` is re-baked
|
|
180
|
+
* by the generator's policy pass (scripts/generated-policies.ts).
|
|
181
|
+
*/
|
|
182
|
+
export const ANTHROPIC_CURATED_FALLBACK_MODELS: readonly ModelSpec<"anthropic-messages">[] = [
|
|
183
|
+
{
|
|
184
|
+
id: "claude-sonnet-5",
|
|
185
|
+
name: "Claude Sonnet 5",
|
|
186
|
+
api: "anthropic-messages",
|
|
187
|
+
provider: "anthropic",
|
|
188
|
+
baseUrl: "https://api.anthropic.com",
|
|
189
|
+
reasoning: true,
|
|
190
|
+
input: ["text", "image"],
|
|
191
|
+
cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
|
192
|
+
contextWindow: 1_000_000,
|
|
193
|
+
maxTokens: 128_000,
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
id: "claude-fable-5",
|
|
197
|
+
name: "Claude Fable 5",
|
|
198
|
+
api: "anthropic-messages",
|
|
199
|
+
provider: "anthropic",
|
|
200
|
+
baseUrl: "https://api.anthropic.com",
|
|
201
|
+
reasoning: true,
|
|
202
|
+
input: ["text", "image"],
|
|
203
|
+
cost: { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 },
|
|
204
|
+
contextWindow: 1_000_000,
|
|
205
|
+
maxTokens: 128_000,
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
id: "claude-mythos-5",
|
|
209
|
+
name: "Claude Mythos 5",
|
|
210
|
+
api: "anthropic-messages",
|
|
211
|
+
provider: "anthropic",
|
|
212
|
+
baseUrl: "https://api.anthropic.com",
|
|
213
|
+
reasoning: true,
|
|
214
|
+
input: ["text", "image"],
|
|
215
|
+
cost: { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 },
|
|
216
|
+
contextWindow: 1_000_000,
|
|
217
|
+
maxTokens: 128_000,
|
|
218
|
+
},
|
|
219
|
+
];
|
|
220
|
+
|
|
221
|
+
function mapWithBundledReference<TApi extends Api>(
|
|
222
|
+
entry: OpenAICompatibleModelRecord,
|
|
223
|
+
defaults: ModelSpec<TApi>,
|
|
224
|
+
reference: ModelSpec<TApi> | undefined,
|
|
225
|
+
): ModelSpec<TApi> {
|
|
226
|
+
const name = toModelName(entry.name, reference?.name ?? defaults.name);
|
|
227
|
+
if (!reference) {
|
|
228
|
+
return {
|
|
229
|
+
...defaults,
|
|
230
|
+
name,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
...reference,
|
|
235
|
+
id: defaults.id,
|
|
236
|
+
name,
|
|
237
|
+
api: defaults.api,
|
|
238
|
+
provider: defaults.provider,
|
|
239
|
+
baseUrl: defaults.baseUrl,
|
|
240
|
+
contextWindow: toPositiveNumber(entry.context_length, reference.contextWindow),
|
|
241
|
+
maxTokens: toPositiveNumber(entry.max_completion_tokens, reference.maxTokens),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function normalizeAnthropicBaseUrl(baseUrl: string | undefined, fallback: string): string {
|
|
246
|
+
const value = baseUrl?.trim();
|
|
247
|
+
if (!value) {
|
|
248
|
+
return fallback;
|
|
249
|
+
}
|
|
250
|
+
return value.endsWith("/") ? value.slice(0, -1) : value;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function toAnthropicDiscoveryBaseUrl(baseUrl: string): string {
|
|
254
|
+
return baseUrl.endsWith("/v1") ? baseUrl : `${baseUrl}/v1`;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function normalizeOllamaBaseUrl(baseUrl?: string): string {
|
|
258
|
+
const value = baseUrl?.trim();
|
|
259
|
+
if (!value) {
|
|
260
|
+
return "http://127.0.0.1:11434/v1";
|
|
261
|
+
}
|
|
262
|
+
const trimmed = value.endsWith("/") ? value.slice(0, -1) : value;
|
|
263
|
+
return trimmed.endsWith("/v1") ? trimmed : `${trimmed}/v1`;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function toOllamaNativeBaseUrl(baseUrl: string): string {
|
|
267
|
+
return baseUrl.endsWith("/v1") ? baseUrl.slice(0, -3) : baseUrl;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function fetchOllamaNativeModels(
|
|
271
|
+
baseUrl: string,
|
|
272
|
+
resolveMetadata: (modelId: string) => Promise<OllamaResolvedMetadata>,
|
|
273
|
+
fetchImpl: FetchImpl = discoveryFetch(),
|
|
274
|
+
): Promise<ModelSpec<"openai-responses">[] | null> {
|
|
275
|
+
const nativeBaseUrl = toOllamaNativeBaseUrl(baseUrl);
|
|
276
|
+
let response: Response;
|
|
277
|
+
try {
|
|
278
|
+
response = await fetchImpl(`${nativeBaseUrl}/api/tags`, {
|
|
279
|
+
method: "GET",
|
|
280
|
+
headers: { Accept: "application/json" },
|
|
281
|
+
});
|
|
282
|
+
} catch {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
if (!response.ok) {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
const payload = (await response.json()) as { models?: Array<{ name?: string; model?: string }> };
|
|
289
|
+
const entries = payload.models ?? [];
|
|
290
|
+
const resolved = await Promise.all(
|
|
291
|
+
entries.map(async (entry): Promise<ModelSpec<"openai-responses"> | null> => {
|
|
292
|
+
const id = entry.model ?? entry.name;
|
|
293
|
+
if (!id) return null;
|
|
294
|
+
const metadata = await resolveMetadata(id);
|
|
295
|
+
return {
|
|
296
|
+
id,
|
|
297
|
+
name: entry.name ?? id,
|
|
298
|
+
api: "openai-responses",
|
|
299
|
+
provider: "ollama",
|
|
300
|
+
baseUrl,
|
|
301
|
+
reasoning: metadata.reasoning ?? false,
|
|
302
|
+
thinking: metadata.thinking,
|
|
303
|
+
input: metadata.input ?? ["text"],
|
|
304
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
305
|
+
contextWindow: metadata.contextWindow,
|
|
306
|
+
maxTokens: metadata.maxTokens,
|
|
307
|
+
};
|
|
308
|
+
}),
|
|
309
|
+
);
|
|
310
|
+
const models: ModelSpec<"openai-responses">[] = resolved.filter(
|
|
311
|
+
(m): m is ModelSpec<"openai-responses"> => m !== null,
|
|
312
|
+
);
|
|
313
|
+
return models.sort((left, right) => left.id.localeCompare(right.id));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Fallback context window for Ollama models when `/api/show` is unavailable
|
|
318
|
+
* or omits a `model_info.<arch>.context_length` field. Matches the size
|
|
319
|
+
* Ollama's cloud catalog reports for stock models.
|
|
320
|
+
*/
|
|
321
|
+
const OLLAMA_FALLBACK_CONTEXT_WINDOW = 128_000;
|
|
322
|
+
/** Cap max output tokens at a value that matches OMP's other openai-responses defaults. */
|
|
323
|
+
const OLLAMA_DEFAULT_MAX_TOKENS = 8192;
|
|
324
|
+
|
|
325
|
+
interface OllamaResolvedMetadata {
|
|
326
|
+
contextWindow: number;
|
|
327
|
+
maxTokens: number;
|
|
328
|
+
capabilities?: string[];
|
|
329
|
+
reasoning?: boolean;
|
|
330
|
+
thinking?: ThinkingConfig;
|
|
331
|
+
input?: ("text" | "image")[];
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
interface OllamaShowMetadata {
|
|
335
|
+
contextWindow?: number;
|
|
336
|
+
maxTokens?: number;
|
|
337
|
+
capabilities?: string[];
|
|
338
|
+
reasoning?: boolean;
|
|
339
|
+
thinking?: ThinkingConfig;
|
|
340
|
+
input?: ("text" | "image")[];
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function getOllamaContextWindow(modelInfo: Record<string, unknown> | undefined): number | undefined {
|
|
344
|
+
if (!modelInfo) {
|
|
345
|
+
return undefined;
|
|
346
|
+
}
|
|
347
|
+
for (const [key, value] of Object.entries(modelInfo)) {
|
|
348
|
+
if (typeof value !== "number" || value <= 0) {
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
if (key.endsWith(".context_length") || key.endsWith(".num_ctx") || key.endsWith(".context_window")) {
|
|
352
|
+
return value;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function getOllamaCapabilities(value: unknown): string[] | undefined {
|
|
358
|
+
if (!Array.isArray(value)) {
|
|
359
|
+
return undefined;
|
|
360
|
+
}
|
|
361
|
+
return value.filter((item): item is string => typeof item === "string");
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function getOllamaThinkingConfig(capabilities: string[] | undefined): ThinkingConfig | undefined {
|
|
365
|
+
if (!capabilities?.includes("thinking")) {
|
|
366
|
+
return undefined;
|
|
367
|
+
}
|
|
368
|
+
return { mode: "effort", efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High] };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Query Ollama's `/api/show` endpoint for a single model and pull native
|
|
373
|
+
* context and capability metadata from the response. Returns `undefined` when
|
|
374
|
+
* the endpoint is unavailable so callers can layer their own fallback.
|
|
375
|
+
*/
|
|
376
|
+
async function fetchOllamaShowMetadata(
|
|
377
|
+
nativeBaseUrl: string,
|
|
378
|
+
modelId: string,
|
|
379
|
+
fetchImpl: FetchImpl = discoveryFetch(),
|
|
380
|
+
): Promise<OllamaShowMetadata | undefined> {
|
|
381
|
+
try {
|
|
382
|
+
const response = await fetchImpl(`${nativeBaseUrl}/api/show`, {
|
|
383
|
+
method: "POST",
|
|
384
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
385
|
+
body: JSON.stringify({ model: modelId }),
|
|
386
|
+
});
|
|
387
|
+
if (!response.ok) {
|
|
388
|
+
return undefined;
|
|
389
|
+
}
|
|
390
|
+
const payload = (await response.json()) as { capabilities?: unknown; model_info?: Record<string, unknown> };
|
|
391
|
+
const capabilities = getOllamaCapabilities(payload.capabilities);
|
|
392
|
+
const contextWindow = getOllamaContextWindow(payload.model_info);
|
|
393
|
+
return {
|
|
394
|
+
contextWindow,
|
|
395
|
+
maxTokens: contextWindow ? OLLAMA_DEFAULT_MAX_TOKENS : undefined,
|
|
396
|
+
capabilities,
|
|
397
|
+
reasoning: capabilities ? capabilities.includes("thinking") : undefined,
|
|
398
|
+
thinking: getOllamaThinkingConfig(capabilities),
|
|
399
|
+
input: capabilities
|
|
400
|
+
? capabilities.includes("vision")
|
|
401
|
+
? (["text", "image"] as Array<"text" | "image">)
|
|
402
|
+
: (["text"] as Array<"text">)
|
|
403
|
+
: undefined,
|
|
404
|
+
};
|
|
405
|
+
} catch {
|
|
406
|
+
// fall through; caller decides on the fallback
|
|
407
|
+
}
|
|
408
|
+
return undefined;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Build a resolver that fetches `/api/show` metadata per model id and caches
|
|
413
|
+
* the result in-memory for the lifetime of the manager. Successful lookups are
|
|
414
|
+
* cached so repeated `fetchDynamicModels` calls do not refetch; failed
|
|
415
|
+
* lookups stay uncached so a later refresh can recover.
|
|
416
|
+
*/
|
|
417
|
+
function createOllamaMetadataResolver(
|
|
418
|
+
nativeBaseUrl: string,
|
|
419
|
+
fetchImpl?: FetchImpl,
|
|
420
|
+
): (modelId: string) => Promise<OllamaResolvedMetadata> {
|
|
421
|
+
const cache = new Map<string, Promise<OllamaResolvedMetadata>>();
|
|
422
|
+
return modelId => {
|
|
423
|
+
const cached = cache.get(modelId);
|
|
424
|
+
if (cached) return cached;
|
|
425
|
+
const pending = (async () => {
|
|
426
|
+
const metadata = await fetchOllamaShowMetadata(nativeBaseUrl, modelId, fetchImpl);
|
|
427
|
+
if (!metadata) {
|
|
428
|
+
cache.delete(modelId);
|
|
429
|
+
return { contextWindow: OLLAMA_FALLBACK_CONTEXT_WINDOW, maxTokens: OLLAMA_DEFAULT_MAX_TOKENS };
|
|
430
|
+
}
|
|
431
|
+
return {
|
|
432
|
+
...metadata,
|
|
433
|
+
contextWindow: metadata.contextWindow ?? OLLAMA_FALLBACK_CONTEXT_WINDOW,
|
|
434
|
+
maxTokens: metadata.maxTokens ?? OLLAMA_DEFAULT_MAX_TOKENS,
|
|
435
|
+
};
|
|
436
|
+
})();
|
|
437
|
+
cache.set(modelId, pending);
|
|
438
|
+
void pending.catch(() => cache.delete(modelId));
|
|
439
|
+
return pending;
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const OPENAI_NON_RESPONSES_PREFIXES = [
|
|
444
|
+
"text-embedding",
|
|
445
|
+
"whisper-",
|
|
446
|
+
"tts-",
|
|
447
|
+
"omni-moderation",
|
|
448
|
+
"omni-transcribe",
|
|
449
|
+
"omni-speech",
|
|
450
|
+
"gpt-image-",
|
|
451
|
+
"gpt-realtime",
|
|
452
|
+
] as const;
|
|
453
|
+
|
|
454
|
+
function isLikelyOpenAIResponsesModelId(id: string, references: Map<string, ModelSpec<"openai-responses">>): boolean {
|
|
455
|
+
const trimmed = id.trim();
|
|
456
|
+
if (!trimmed) {
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
if (references.has(trimmed)) {
|
|
460
|
+
return true;
|
|
461
|
+
}
|
|
462
|
+
const normalized = trimmed.toLowerCase();
|
|
463
|
+
if (OPENAI_NON_RESPONSES_PREFIXES.some(prefix => normalized.startsWith(prefix))) {
|
|
464
|
+
return false;
|
|
465
|
+
}
|
|
466
|
+
if (normalized.includes("embedding")) {
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
return (
|
|
470
|
+
normalized.startsWith("gpt-") ||
|
|
471
|
+
normalized.startsWith("o1") ||
|
|
472
|
+
normalized.startsWith("o3") ||
|
|
473
|
+
normalized.startsWith("o4") ||
|
|
474
|
+
normalized.startsWith("chatgpt")
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const NANO_GPT_NON_TEXT_MODEL_TOKENS = [
|
|
479
|
+
"embedding",
|
|
480
|
+
"image",
|
|
481
|
+
"vision",
|
|
482
|
+
"audio",
|
|
483
|
+
"speech",
|
|
484
|
+
"transcribe",
|
|
485
|
+
"moderation",
|
|
486
|
+
"realtime",
|
|
487
|
+
"whisper",
|
|
488
|
+
"tts",
|
|
489
|
+
] as const;
|
|
490
|
+
|
|
491
|
+
/** Regex matching NanoGPT `:thinking` suffixed model IDs (with or without a level). */
|
|
492
|
+
const NANO_GPT_THINKING_SUFFIX_RE = /:thinking(:[^:]+)?$/;
|
|
493
|
+
|
|
494
|
+
function isLikelyNanoGptTextModelId(id: string): boolean {
|
|
495
|
+
const normalized = id.trim().toLowerCase();
|
|
496
|
+
if (!normalized) {
|
|
497
|
+
return false;
|
|
498
|
+
}
|
|
499
|
+
if (NANO_GPT_THINKING_SUFFIX_RE.test(normalized)) {
|
|
500
|
+
return false;
|
|
501
|
+
}
|
|
502
|
+
return !NANO_GPT_NON_TEXT_MODEL_TOKENS.some(token => normalized.includes(token));
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
type SimpleProviderDiscoveryHeaders = Record<string, string> | (() => Record<string, string> | undefined);
|
|
506
|
+
type SimpleProviderConfig = {
|
|
507
|
+
apiKey?: string;
|
|
508
|
+
baseUrl?: string;
|
|
509
|
+
fetch?: FetchImpl;
|
|
510
|
+
headers?: SimpleProviderDiscoveryHeaders;
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
function resolveSimpleProviderHeaders(
|
|
514
|
+
headers: SimpleProviderDiscoveryHeaders | undefined,
|
|
515
|
+
): Record<string, string> | undefined {
|
|
516
|
+
return typeof headers === "function" ? headers() : headers;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
export function createSimpleOpenAICompletionsOptions(
|
|
520
|
+
providerId: Parameters<typeof getBundledModels>[0],
|
|
521
|
+
defaultBaseUrl: string,
|
|
522
|
+
config?: SimpleProviderConfig,
|
|
523
|
+
): ModelManagerOptions<"openai-completions"> {
|
|
524
|
+
const apiKey = config?.apiKey;
|
|
525
|
+
const baseUrl = config?.baseUrl ?? defaultBaseUrl;
|
|
526
|
+
const references = createBundledReferenceMap<"openai-completions">(providerId);
|
|
527
|
+
return {
|
|
528
|
+
providerId,
|
|
529
|
+
...(apiKey && {
|
|
530
|
+
fetchDynamicModels: () =>
|
|
531
|
+
fetchOpenAICompatibleModels({
|
|
532
|
+
api: "openai-completions",
|
|
533
|
+
provider: providerId,
|
|
534
|
+
baseUrl,
|
|
535
|
+
apiKey,
|
|
536
|
+
headers: resolveSimpleProviderHeaders(config?.headers),
|
|
537
|
+
mapModel: (entry, defaults) => {
|
|
538
|
+
const reference = references.get(defaults.id);
|
|
539
|
+
return mapWithBundledReference(entry, defaults, reference);
|
|
540
|
+
},
|
|
541
|
+
fetch: config?.fetch,
|
|
542
|
+
}),
|
|
543
|
+
}),
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function createSimpleOpenAIResponsesOptions(
|
|
548
|
+
providerId: Parameters<typeof getBundledModels>[0],
|
|
549
|
+
defaultBaseUrl: string,
|
|
550
|
+
config?: SimpleProviderConfig,
|
|
551
|
+
): ModelManagerOptions<"openai-responses"> {
|
|
552
|
+
const apiKey = config?.apiKey;
|
|
553
|
+
const baseUrl = config?.baseUrl ?? defaultBaseUrl;
|
|
554
|
+
const references = createBundledReferenceMap<"openai-responses">(providerId);
|
|
555
|
+
return {
|
|
556
|
+
providerId,
|
|
557
|
+
...(apiKey && {
|
|
558
|
+
fetchDynamicModels: () =>
|
|
559
|
+
fetchOpenAICompatibleModels({
|
|
560
|
+
api: "openai-responses",
|
|
561
|
+
provider: providerId,
|
|
562
|
+
baseUrl,
|
|
563
|
+
apiKey,
|
|
564
|
+
headers: resolveSimpleProviderHeaders(config?.headers),
|
|
565
|
+
mapModel: (entry, defaults) => {
|
|
566
|
+
const reference = references.get(defaults.id);
|
|
567
|
+
return mapWithBundledReference(entry, defaults, reference);
|
|
568
|
+
},
|
|
569
|
+
fetch: config?.fetch,
|
|
570
|
+
}),
|
|
571
|
+
}),
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function createSimpleAnthropicProviderOptions(
|
|
576
|
+
providerId: Parameters<typeof getBundledModels>[0],
|
|
577
|
+
defaultBaseUrlFallback: string,
|
|
578
|
+
config?: SimpleProviderConfig,
|
|
579
|
+
): ModelManagerOptions<"anthropic-messages"> {
|
|
580
|
+
const apiKey = config?.apiKey;
|
|
581
|
+
const baseUrl = normalizeAnthropicBaseUrl(config?.baseUrl, defaultBaseUrlFallback);
|
|
582
|
+
const discoveryBaseUrl = toAnthropicDiscoveryBaseUrl(baseUrl);
|
|
583
|
+
const references = createBundledReferenceMap<"anthropic-messages">(providerId);
|
|
584
|
+
return {
|
|
585
|
+
providerId,
|
|
586
|
+
...(apiKey && {
|
|
587
|
+
fetchDynamicModels: () =>
|
|
588
|
+
fetchOpenAICompatibleModels({
|
|
589
|
+
api: "anthropic-messages",
|
|
590
|
+
provider: providerId,
|
|
591
|
+
baseUrl: discoveryBaseUrl,
|
|
592
|
+
headers: buildAnthropicDiscoveryHeaders(apiKey),
|
|
593
|
+
mapModel: (entry, defaults) => {
|
|
594
|
+
const reference = references.get(defaults.id);
|
|
595
|
+
const model = mapWithBundledReference(entry, defaults, reference);
|
|
596
|
+
return {
|
|
597
|
+
...model,
|
|
598
|
+
name: toModelName(entry.display_name, model.name),
|
|
599
|
+
baseUrl,
|
|
600
|
+
};
|
|
601
|
+
},
|
|
602
|
+
fetch: config?.fetch,
|
|
603
|
+
}),
|
|
604
|
+
}),
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// ---------------------------------------------------------------------------
|
|
609
|
+
// Umans AI Coding Plan
|
|
610
|
+
// ---------------------------------------------------------------------------
|
|
611
|
+
|
|
612
|
+
const UMANS_BASE_URL = "https://api.code.umans.ai";
|
|
613
|
+
const UMANS_MODELS_INFO_PATH = "/models/info";
|
|
614
|
+
const UMANS_REASONING_EFFORT_BY_LEVEL: Record<string, Effort> = {
|
|
615
|
+
minimal: Effort.Minimal,
|
|
616
|
+
low: Effort.Low,
|
|
617
|
+
medium: Effort.Medium,
|
|
618
|
+
high: Effort.High,
|
|
619
|
+
xhigh: Effort.XHigh,
|
|
620
|
+
max: Effort.XHigh,
|
|
621
|
+
};
|
|
622
|
+
const UMANS_MAX_REASONING_EFFORT_MAP = { [Effort.XHigh]: "max" } as const;
|
|
623
|
+
const UMANS_DEFAULT_REASONING_EFFORTS = [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High, Effort.XHigh] as const;
|
|
624
|
+
const UMANS_VIA_HANDOFF_MODEL_IDS = ["umans-glm-5.1", "umans-glm-5.2"] as const;
|
|
625
|
+
|
|
626
|
+
export interface UmansModelManagerConfig {
|
|
627
|
+
apiKey?: string;
|
|
628
|
+
baseUrl?: string;
|
|
629
|
+
fetch?: FetchImpl;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
interface UmansModelInfo {
|
|
633
|
+
name?: unknown;
|
|
634
|
+
display_name?: unknown;
|
|
635
|
+
capabilities?: unknown;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function normalizeUmansBaseUrl(baseUrl: string | undefined): string {
|
|
639
|
+
const normalized = normalizeAnthropicBaseUrl(baseUrl, UMANS_BASE_URL);
|
|
640
|
+
return normalized.endsWith("/v1") ? normalized.slice(0, -3) : normalized;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Umans `models/info` reports `supports_vision: true` for natively
|
|
645
|
+
* vision-capable models and a non-empty string sentinel (e.g.
|
|
646
|
+
* `"via-handoff"`) for models that route image inputs through a vision
|
|
647
|
+
* handoff pre-analysis step instead of accepting raw image blocks. Only
|
|
648
|
+
* `true` means the model accepts image content directly; sentinel values
|
|
649
|
+
* MUST map to text-only so the agent's vision-handoff path runs instead
|
|
650
|
+
* of triggering an upstream HTTP 400 (`This model does not support image
|
|
651
|
+
* inputs`).
|
|
652
|
+
*/
|
|
653
|
+
function umansSupportsVision(value: unknown): boolean {
|
|
654
|
+
return value === true;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function umansReasoningSupported(value: unknown): boolean {
|
|
658
|
+
return isRecord(value) ? value.supported === true : value === true;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function mapUmansReasoningEfforts(value: unknown): readonly Effort[] {
|
|
662
|
+
if (!isRecord(value) || !Array.isArray(value.levels)) {
|
|
663
|
+
return UMANS_DEFAULT_REASONING_EFFORTS;
|
|
664
|
+
}
|
|
665
|
+
const efforts: Effort[] = [];
|
|
666
|
+
for (const level of value.levels) {
|
|
667
|
+
if (typeof level !== "string") continue;
|
|
668
|
+
const effort = UMANS_REASONING_EFFORT_BY_LEVEL[level];
|
|
669
|
+
if (effort !== undefined && !efforts.includes(effort)) {
|
|
670
|
+
efforts.push(effort);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return efforts.length > 0 ? efforts : UMANS_DEFAULT_REASONING_EFFORTS;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function umansHasMaxReasoningLevel(value: unknown): boolean {
|
|
677
|
+
return isRecord(value) && Array.isArray(value.levels) && value.levels.includes("max");
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function mapUmansThinkingConfig(value: unknown): ThinkingConfig | undefined {
|
|
681
|
+
if (!umansReasoningSupported(value)) return undefined;
|
|
682
|
+
const efforts = mapUmansReasoningEfforts(value);
|
|
683
|
+
const thinking: ThinkingConfig = {
|
|
684
|
+
mode: umansHasMaxReasoningLevel(value) ? "anthropic-budget-effort" : "budget",
|
|
685
|
+
efforts,
|
|
686
|
+
};
|
|
687
|
+
if (thinking.mode === "anthropic-budget-effort") {
|
|
688
|
+
thinking.effortMap = UMANS_MAX_REASONING_EFFORT_MAP;
|
|
689
|
+
}
|
|
690
|
+
if (isRecord(value)) {
|
|
691
|
+
if (value.can_disable === false) {
|
|
692
|
+
thinking.requiresEffort = true;
|
|
693
|
+
}
|
|
694
|
+
if (typeof value.default_level === "string") {
|
|
695
|
+
const defaultLevel = UMANS_REASONING_EFFORT_BY_LEVEL[value.default_level];
|
|
696
|
+
if (defaultLevel !== undefined && efforts.includes(defaultLevel)) {
|
|
697
|
+
thinking.defaultLevel = defaultLevel;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
return thinking;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function mapUmansModelInfo(
|
|
705
|
+
modelId: string,
|
|
706
|
+
raw: UmansModelInfo,
|
|
707
|
+
baseUrl: string,
|
|
708
|
+
reference: ModelSpec<"anthropic-messages"> | undefined,
|
|
709
|
+
): ModelSpec<"anthropic-messages"> | null {
|
|
710
|
+
if (!modelId) return null;
|
|
711
|
+
const capabilities = isRecord(raw.capabilities) ? raw.capabilities : {};
|
|
712
|
+
const supportsTools = capabilities.supports_tools;
|
|
713
|
+
const thinking = mapUmansThinkingConfig(capabilities.reasoning);
|
|
714
|
+
return {
|
|
715
|
+
...reference,
|
|
716
|
+
id: modelId,
|
|
717
|
+
name: toModelName(raw.display_name, toModelName(raw.name, modelId)),
|
|
718
|
+
api: "anthropic-messages",
|
|
719
|
+
provider: "umans",
|
|
720
|
+
baseUrl,
|
|
721
|
+
compat: { ...reference?.compat, escapeBuiltinToolNames: true },
|
|
722
|
+
reasoning: thinking !== undefined,
|
|
723
|
+
...(thinking ? { thinking } : {}),
|
|
724
|
+
input: umansSupportsVision(capabilities.supports_vision) ? ["text", "image"] : ["text"],
|
|
725
|
+
...(supportsTools === false ? { supportsTools: false } : {}),
|
|
726
|
+
cost: reference?.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
727
|
+
contextWindow: toPositiveNumber(capabilities.context_window, reference?.contextWindow ?? null),
|
|
728
|
+
maxTokens: toPositiveNumber(
|
|
729
|
+
capabilities.recommended_max_tokens,
|
|
730
|
+
toPositiveNumber(capabilities.max_completion_tokens, reference?.maxTokens ?? null),
|
|
731
|
+
),
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
async function fetchUmansModelsInfo(options: {
|
|
736
|
+
baseUrl: string;
|
|
737
|
+
apiKey?: string;
|
|
738
|
+
fetch?: FetchImpl;
|
|
739
|
+
references: Map<string, ModelSpec<"anthropic-messages">>;
|
|
740
|
+
}): Promise<ModelSpec<"anthropic-messages">[] | null> {
|
|
741
|
+
const discoveryBaseUrl = toAnthropicDiscoveryBaseUrl(options.baseUrl);
|
|
742
|
+
const requestHeaders: Record<string, string> = { Accept: "application/json" };
|
|
743
|
+
if (options.apiKey) {
|
|
744
|
+
requestHeaders["x-api-key"] = options.apiKey;
|
|
745
|
+
}
|
|
746
|
+
const fetchImpl = discoveryFetch(options.fetch);
|
|
747
|
+
let payload: unknown;
|
|
748
|
+
try {
|
|
749
|
+
const response = await fetchImpl(`${discoveryBaseUrl}${UMANS_MODELS_INFO_PATH}`, {
|
|
750
|
+
method: "GET",
|
|
751
|
+
headers: requestHeaders,
|
|
752
|
+
});
|
|
753
|
+
if (!response.ok) {
|
|
754
|
+
return null;
|
|
755
|
+
}
|
|
756
|
+
payload = await response.json();
|
|
757
|
+
} catch (error) {
|
|
758
|
+
throw new Error("Failed to fetch Umans models info", { cause: error });
|
|
759
|
+
}
|
|
760
|
+
if (!isRecord(payload)) {
|
|
761
|
+
return null;
|
|
762
|
+
}
|
|
763
|
+
const models: ModelSpec<"anthropic-messages">[] = [];
|
|
764
|
+
for (const [modelId, value] of Object.entries(payload)) {
|
|
765
|
+
if (!isRecord(value)) continue;
|
|
766
|
+
const mapped = mapUmansModelInfo(modelId, value, options.baseUrl, options.references.get(modelId));
|
|
767
|
+
if (mapped) {
|
|
768
|
+
models.push(mapped);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
return models.sort((left, right) => left.id.localeCompare(right.id));
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
export function umansModelManagerOptions(config?: UmansModelManagerConfig): ModelManagerOptions<"anthropic-messages"> {
|
|
775
|
+
const apiKey = config?.apiKey;
|
|
776
|
+
const baseUrl = normalizeUmansBaseUrl(config?.baseUrl);
|
|
777
|
+
const references = createBundledReferenceMap<"anthropic-messages">("umans");
|
|
778
|
+
return {
|
|
779
|
+
providerId: "umans",
|
|
780
|
+
dynamicModelsAuthoritative: true,
|
|
781
|
+
dropCachedModelIdsOnStaticMismatch: UMANS_VIA_HANDOFF_MODEL_IDS,
|
|
782
|
+
fetchDynamicModels: () => fetchUmansModelsInfo({ baseUrl, apiKey, fetch: config?.fetch, references }),
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
// ---------------------------------------------------------------------------
|
|
786
|
+
// 1. OpenAI
|
|
787
|
+
// ---------------------------------------------------------------------------
|
|
788
|
+
|
|
789
|
+
export interface OpenAIModelManagerConfig {
|
|
790
|
+
apiKey?: string;
|
|
791
|
+
baseUrl?: string;
|
|
792
|
+
fetch?: FetchImpl;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
export function openaiModelManagerOptions(config?: OpenAIModelManagerConfig): ModelManagerOptions<"openai-responses"> {
|
|
796
|
+
const apiKey = config?.apiKey;
|
|
797
|
+
const baseUrl = config?.baseUrl ?? "https://api.openai.com/v1";
|
|
798
|
+
const references = createBundledReferenceMap<"openai-responses">("openai");
|
|
799
|
+
return {
|
|
800
|
+
providerId: "openai",
|
|
801
|
+
...(apiKey && {
|
|
802
|
+
fetchDynamicModels: () =>
|
|
803
|
+
fetchOpenAICompatibleModels({
|
|
804
|
+
api: "openai-responses",
|
|
805
|
+
provider: "openai",
|
|
806
|
+
baseUrl,
|
|
807
|
+
apiKey,
|
|
808
|
+
filterModel: (_entry, model) => isLikelyOpenAIResponsesModelId(model.id, references),
|
|
809
|
+
mapModel: (entry, defaults) => {
|
|
810
|
+
const reference = references.get(defaults.id);
|
|
811
|
+
return mapWithBundledReference(entry, defaults, reference);
|
|
812
|
+
},
|
|
813
|
+
fetch: config?.fetch,
|
|
814
|
+
}),
|
|
815
|
+
}),
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// ---------------------------------------------------------------------------
|
|
820
|
+
// 2. Groq
|
|
821
|
+
// ---------------------------------------------------------------------------
|
|
822
|
+
|
|
823
|
+
export interface GroqModelManagerConfig {
|
|
824
|
+
apiKey?: string;
|
|
825
|
+
baseUrl?: string;
|
|
826
|
+
fetch?: FetchImpl;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
export function groqModelManagerOptions(config?: GroqModelManagerConfig): ModelManagerOptions<"openai-completions"> {
|
|
830
|
+
return createSimpleOpenAICompletionsOptions("groq", "https://api.groq.com/openai/v1", config);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// ---------------------------------------------------------------------------
|
|
834
|
+
// 3. Cerebras
|
|
835
|
+
// ---------------------------------------------------------------------------
|
|
836
|
+
|
|
837
|
+
const CEREBRAS_IMAGE_INPUT_MODEL_IDS = new Set(["gemma-4-31b"]);
|
|
838
|
+
|
|
839
|
+
function applyCerebrasDiscoveryOverrides(model: ModelSpec<"openai-completions">): ModelSpec<"openai-completions"> {
|
|
840
|
+
if (!CEREBRAS_IMAGE_INPUT_MODEL_IDS.has(model.id)) {
|
|
841
|
+
return model;
|
|
842
|
+
}
|
|
843
|
+
return {
|
|
844
|
+
...model,
|
|
845
|
+
input: ["text", "image"],
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
export interface CerebrasModelManagerConfig {
|
|
850
|
+
apiKey?: string;
|
|
851
|
+
baseUrl?: string;
|
|
852
|
+
fetch?: FetchImpl;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
export function cerebrasModelManagerOptions(
|
|
856
|
+
config?: CerebrasModelManagerConfig,
|
|
857
|
+
): ModelManagerOptions<"openai-completions"> {
|
|
858
|
+
const apiKey = config?.apiKey;
|
|
859
|
+
const baseUrl = config?.baseUrl ?? "https://api.cerebras.ai/v1";
|
|
860
|
+
const references = createBundledReferenceMap<"openai-completions">("cerebras");
|
|
861
|
+
return {
|
|
862
|
+
providerId: "cerebras",
|
|
863
|
+
...(apiKey && {
|
|
864
|
+
fetchDynamicModels: () =>
|
|
865
|
+
fetchOpenAICompatibleModels({
|
|
866
|
+
api: "openai-completions",
|
|
867
|
+
provider: "cerebras",
|
|
868
|
+
baseUrl,
|
|
869
|
+
apiKey,
|
|
870
|
+
mapModel: (entry, defaults) => {
|
|
871
|
+
const reference = references.get(defaults.id);
|
|
872
|
+
const model = mapWithBundledReference(entry, defaults, reference);
|
|
873
|
+
return applyCerebrasDiscoveryOverrides(model);
|
|
874
|
+
},
|
|
875
|
+
fetch: config?.fetch,
|
|
876
|
+
}),
|
|
877
|
+
}),
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// ---------------------------------------------------------------------------
|
|
882
|
+
// 4. Hugging Face
|
|
883
|
+
// ---------------------------------------------------------------------------
|
|
884
|
+
|
|
885
|
+
export interface HuggingfaceModelManagerConfig {
|
|
886
|
+
apiKey?: string;
|
|
887
|
+
baseUrl?: string;
|
|
888
|
+
fetch?: FetchImpl;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
export function huggingfaceModelManagerOptions(
|
|
892
|
+
config?: HuggingfaceModelManagerConfig,
|
|
893
|
+
): ModelManagerOptions<"openai-completions"> {
|
|
894
|
+
return createSimpleOpenAICompletionsOptions("huggingface", "https://router.huggingface.co/v1", config);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// ---------------------------------------------------------------------------
|
|
898
|
+
// 5. NVIDIA
|
|
899
|
+
// ---------------------------------------------------------------------------
|
|
900
|
+
|
|
901
|
+
export interface NvidiaModelManagerConfig {
|
|
902
|
+
apiKey?: string;
|
|
903
|
+
baseUrl?: string;
|
|
904
|
+
fetch?: FetchImpl;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
export function nvidiaModelManagerOptions(
|
|
908
|
+
config?: NvidiaModelManagerConfig,
|
|
909
|
+
): ModelManagerOptions<"openai-completions"> {
|
|
910
|
+
return createSimpleOpenAICompletionsOptions("nvidia", "https://integrate.api.nvidia.com/v1", config);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// ---------------------------------------------------------------------------
|
|
914
|
+
// 6. xAI
|
|
915
|
+
// ---------------------------------------------------------------------------
|
|
916
|
+
|
|
917
|
+
export interface XaiModelManagerConfig {
|
|
918
|
+
apiKey?: string;
|
|
919
|
+
baseUrl?: string;
|
|
920
|
+
fetch?: FetchImpl;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
export function xaiModelManagerOptions(config?: XaiModelManagerConfig): ModelManagerOptions<"openai-completions"> {
|
|
924
|
+
return createSimpleOpenAICompletionsOptions("xai", "https://api.x.ai/v1", config);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
export interface XaiOAuthModelManagerConfig {
|
|
928
|
+
apiKey?: string;
|
|
929
|
+
baseUrl?: string;
|
|
930
|
+
fetch?: FetchImpl;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
interface XAICuratedModel {
|
|
934
|
+
id: string;
|
|
935
|
+
contextWindow: number;
|
|
936
|
+
name?: string;
|
|
937
|
+
/** Whether the model reasons natively. Defaults to true for Grok-4.x family. */
|
|
938
|
+
reasoning?: boolean;
|
|
939
|
+
/**
|
|
940
|
+
* Whether xAI accepts the `reasoning.effort` wire param for this model.
|
|
941
|
+
* Default true. When false: the picker hides the effort dial (via
|
|
942
|
+
* getSupportedEfforts in model-thinking.ts) AND the wire omits the param —
|
|
943
|
+
* both derive from `isGrokReasoningEffortCapable` (identity/family.ts), the
|
|
944
|
+
* single allowlist shared by this curated layer and the compat builder.
|
|
945
|
+
*/
|
|
946
|
+
supportsReasoningEffort?: boolean;
|
|
947
|
+
/**
|
|
948
|
+
* Input modalities this model accepts. Defaults to `["text"]` when absent.
|
|
949
|
+
* Vision-capable Grok models MUST list `"image"` here so the curated layer
|
|
950
|
+
* overrides `fetchOpenAICompatibleModels`' default of `["text"]` (which
|
|
951
|
+
* otherwise strips image capability on every online refresh).
|
|
952
|
+
*/
|
|
953
|
+
input?: ("text" | "image")[];
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// Source of truth for the xai-oauth chat picker. Top of list = headline.
|
|
957
|
+
// Context windows from hermes-agent/agent/model_metadata.py:205-220
|
|
958
|
+
// ("Values sourced from models.dev (2026-04)"). grok-build is xAI's
|
|
959
|
+
// coding-fine-tuned chat model; 512K context per user spec (2026-05-17).
|
|
960
|
+
//
|
|
961
|
+
// supportsReasoningEffort=false entries reason natively but reject the wire
|
|
962
|
+
// `reasoning.effort` param (api.x.ai returns HTTP 400). The corresponding
|
|
963
|
+
// omit/include/history replay defaults live in catalog compat so every
|
|
964
|
+
// OpenAI-family endpoint consumes the same constraint.
|
|
965
|
+
export const XAI_OAUTH_CURATED_MODELS: readonly XAICuratedModel[] = [
|
|
966
|
+
{
|
|
967
|
+
id: "grok-build",
|
|
968
|
+
contextWindow: 512_000,
|
|
969
|
+
name: "Grok Build",
|
|
970
|
+
supportsReasoningEffort: false,
|
|
971
|
+
input: ["text", "image"],
|
|
972
|
+
},
|
|
973
|
+
{
|
|
974
|
+
id: "grok-build-0.1",
|
|
975
|
+
contextWindow: 256_000,
|
|
976
|
+
name: "Grok Build 0.1",
|
|
977
|
+
supportsReasoningEffort: false,
|
|
978
|
+
input: ["text", "image"],
|
|
979
|
+
},
|
|
980
|
+
{ id: "grok-4.3", contextWindow: 1_000_000, name: "Grok 4.3", input: ["text", "image"] },
|
|
981
|
+
// grok-4.20-multi-agent-0309 is text-only per the bundled catalog; omit `input` for the default.
|
|
982
|
+
{ id: "grok-4.20-multi-agent-0309", contextWindow: 2_000_000, name: "Grok 4.20 (Multi-Agent)" },
|
|
983
|
+
{
|
|
984
|
+
id: "grok-4.20-0309-reasoning",
|
|
985
|
+
contextWindow: 2_000_000,
|
|
986
|
+
name: "Grok 4.20 (Reasoning)",
|
|
987
|
+
supportsReasoningEffort: false,
|
|
988
|
+
input: ["text", "image"],
|
|
989
|
+
},
|
|
990
|
+
{
|
|
991
|
+
id: "grok-4.20-0309-non-reasoning",
|
|
992
|
+
contextWindow: 2_000_000,
|
|
993
|
+
name: "Grok 4.20 (Non-Reasoning)",
|
|
994
|
+
reasoning: false,
|
|
995
|
+
input: ["text", "image"],
|
|
996
|
+
},
|
|
997
|
+
// Cursor's "Composer 2.5 Fast" exposed via SuperGrok: non-reasoning,
|
|
998
|
+
// text-only, 200K context (mirrors Cursor's composer-* catalog entries).
|
|
999
|
+
// Off the Grok effort-capable allowlist; reasoning:false also hides the effort dial.
|
|
1000
|
+
{
|
|
1001
|
+
id: "grok-composer-2.5-fast",
|
|
1002
|
+
contextWindow: 200_000,
|
|
1003
|
+
name: "Grok Composer 2.5 Fast",
|
|
1004
|
+
reasoning: false,
|
|
1005
|
+
input: ["text"],
|
|
1006
|
+
},
|
|
1007
|
+
] as const;
|
|
1008
|
+
|
|
1009
|
+
// xAI /v1/models returns chat, image, voice, and STT entries. Tool surfaces
|
|
1010
|
+
// route through dedicated tools (generate_image, tts) with their own model
|
|
1011
|
+
// strings; the chat picker MUST exclude these prefixes or selecting them 400s.
|
|
1012
|
+
const XAI_NON_CHAT_PREFIXES = ["grok-imagine-", "grok-stt-", "grok-voice-"] as const;
|
|
1013
|
+
|
|
1014
|
+
function withXaiOAuthCompatDefaults(model: ModelSpec<"openai-responses">): ModelSpec<"openai-responses"> {
|
|
1015
|
+
const compat = {
|
|
1016
|
+
...(model.compat ?? {}),
|
|
1017
|
+
includeEncryptedReasoning: model.compat?.includeEncryptedReasoning ?? false,
|
|
1018
|
+
filterReasoningHistory: model.compat?.filterReasoningHistory ?? true,
|
|
1019
|
+
omitReasoningEffort: model.compat?.omitReasoningEffort ?? !isGrokReasoningEffortCapable(model.id),
|
|
1020
|
+
};
|
|
1021
|
+
return { ...model, compat };
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// Hermes-agent parity: only the `minimal -> low` clamp is applied (see
|
|
1025
|
+
// hermes-agent/agent/transports/codex.py:92 `_effort_clamp = {"minimal":
|
|
1026
|
+
// "low"}`). Hermes sends `xhigh` to xAI verbatim and we match that contract
|
|
1027
|
+
// — let xAI decide if the level is valid for the specific Grok model.
|
|
1028
|
+
// `resolveModelThinking` folds this into `model.thinking.effortMap`, downstream
|
|
1029
|
+
// of the omitReasoningEffort gate in pi-ai's stream.ts.
|
|
1030
|
+
const XAI_REASONING_EFFORT_MAP = { minimal: "low" } as const;
|
|
1031
|
+
|
|
1032
|
+
// xai-oauth's /v1/models exposes no per-request output limit on the OAuth
|
|
1033
|
+
// (Grok Build / SuperGrok) surface, so the curated catalog owns `maxTokens`
|
|
1034
|
+
// like it owns `contextWindow`: each entry mirrors its context window. The
|
|
1035
|
+
// openai-responses wire clamps the actual request to
|
|
1036
|
+
// min(requested, model.maxTokens, OPENAI_MAX_OUTPUT_TOKENS=64000), so this is
|
|
1037
|
+
// just "no model-specific sub-cap below 64k", not an unbounded output budget.
|
|
1038
|
+
|
|
1039
|
+
// Single source of truth for curated → Model fan-in. Used by the static-seed
|
|
1040
|
+
// and the dynamic overlay/inject paths (applyXAIOAuthCuration) so curated
|
|
1041
|
+
// reasoning/effort flags survive an online refresh (xAI's /v1/models lacks
|
|
1042
|
+
// reasoning metadata and fetchOpenAICompatibleModels defaults reasoning to
|
|
1043
|
+
// false). Caller supplies a `base` Model (either a freshly synthesised seed
|
|
1044
|
+
// or a dynamic-fetched entry); the helper layers curated fields on top.
|
|
1045
|
+
// The `minimal -> low` effort clamp (XAI_REASONING_EFFORT_MAP) is always
|
|
1046
|
+
// merged in so dynamic-fetched models — which arrive without curated
|
|
1047
|
+
// compat keys — still get the clamp applyResponsesReasoningParams expects.
|
|
1048
|
+
function mergeCuratedIntoModel(
|
|
1049
|
+
base: ModelSpec<"openai-responses">,
|
|
1050
|
+
curated: XAICuratedModel,
|
|
1051
|
+
): ModelSpec<"openai-responses"> {
|
|
1052
|
+
const effort = curated.supportsReasoningEffort;
|
|
1053
|
+
const compat = {
|
|
1054
|
+
...(base.compat ?? {}),
|
|
1055
|
+
reasoningEffortMap: { ...XAI_REASONING_EFFORT_MAP, ...(base.compat?.reasoningEffortMap ?? {}) },
|
|
1056
|
+
includeEncryptedReasoning: base.compat?.includeEncryptedReasoning ?? false,
|
|
1057
|
+
filterReasoningHistory: base.compat?.filterReasoningHistory ?? true,
|
|
1058
|
+
omitReasoningEffort: base.compat?.omitReasoningEffort ?? !isGrokReasoningEffortCapable(base.id),
|
|
1059
|
+
...(effort === undefined ? {} : { supportsReasoningEffort: effort }),
|
|
1060
|
+
};
|
|
1061
|
+
return {
|
|
1062
|
+
...base,
|
|
1063
|
+
contextWindow: curated.contextWindow,
|
|
1064
|
+
maxTokens: curated.contextWindow,
|
|
1065
|
+
name: curated.name ?? base.name,
|
|
1066
|
+
reasoning: curated.reasoning ?? true,
|
|
1067
|
+
input: curated.input ?? base.input,
|
|
1068
|
+
compat,
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
/**
|
|
1073
|
+
* Overlay/inject curated xai-oauth metadata onto dynamic-fetch results so
|
|
1074
|
+
* a successful `online refresh` doesn't regress vision capability, context
|
|
1075
|
+
* window, reasoning flags, or the effort-dial allowlist.
|
|
1076
|
+
*
|
|
1077
|
+
* Three passes:
|
|
1078
|
+
* 1. Filter `XAI_NON_CHAT_PREFIXES` (picker pollution defense for tool
|
|
1079
|
+
* surfaces routed through dedicated tools — generate_image, tts).
|
|
1080
|
+
* 2. Overlay curated metadata onto dynamic-fetch matches. xAI's /v1/models
|
|
1081
|
+
* does not return context_window or reasoning metadata, so without
|
|
1082
|
+
* this overlay the runtime falls back to the bundled-reference default
|
|
1083
|
+
* (effectively 128k context) and `reasoning: false` (suppressing the
|
|
1084
|
+
* effort dial and stripping thinking metadata downstream).
|
|
1085
|
+
* 3. Inject curated entries missing from the dynamic fetch. Clones the
|
|
1086
|
+
* first surviving entry as a template so required Model fields (api,
|
|
1087
|
+
* provider, baseUrl, cost, etc.) inherit sane defaults. If `filtered`
|
|
1088
|
+
* is empty (offline / no auth) injection is skipped — the descriptor's
|
|
1089
|
+
* defaultModel covers the fallback.
|
|
1090
|
+
*
|
|
1091
|
+
* Order: curated models first in declaration order; then dynamic remainder
|
|
1092
|
+
* in original order.
|
|
1093
|
+
*/
|
|
1094
|
+
function applyXAIOAuthCuration(dynamic: readonly ModelSpec<"openai-responses">[]): ModelSpec<"openai-responses">[] {
|
|
1095
|
+
const filtered = dynamic.filter(e => !XAI_NON_CHAT_PREFIXES.some(p => e.id.startsWith(p)));
|
|
1096
|
+
|
|
1097
|
+
const byId = new Map<string, ModelSpec<"openai-responses">>(filtered.map(e => [e.id, e]));
|
|
1098
|
+
for (const curated of XAI_OAUTH_CURATED_MODELS) {
|
|
1099
|
+
const existing = byId.get(curated.id);
|
|
1100
|
+
if (existing) {
|
|
1101
|
+
byId.set(curated.id, mergeCuratedIntoModel(existing, curated));
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
const template = filtered[0];
|
|
1106
|
+
if (template) {
|
|
1107
|
+
for (const curated of XAI_OAUTH_CURATED_MODELS) {
|
|
1108
|
+
if (!byId.has(curated.id)) {
|
|
1109
|
+
// Reset id/name on the template before merging so the helper's
|
|
1110
|
+
// `curated.name ?? base.name` clause falls back to curated.id
|
|
1111
|
+
// (the inject contract), not to the unrelated template's label.
|
|
1112
|
+
const base: ModelSpec<"openai-responses"> = { ...template, id: curated.id, name: curated.id };
|
|
1113
|
+
byId.set(curated.id, mergeCuratedIntoModel(base, curated));
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
const curatedIds = new Set(XAI_OAUTH_CURATED_MODELS.map(c => c.id));
|
|
1119
|
+
const curatedFirst = XAI_OAUTH_CURATED_MODELS.map(c => byId.get(c.id)).filter(
|
|
1120
|
+
(e): e is ModelSpec<"openai-responses"> => e !== undefined,
|
|
1121
|
+
);
|
|
1122
|
+
const rest = filtered.filter(e => !curatedIds.has(e.id)).map(withXaiOAuthCompatDefaults);
|
|
1123
|
+
return [...curatedFirst, ...rest];
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
/**
|
|
1127
|
+
* Render `XAI_OAUTH_CURATED_MODELS` as full `ModelSpec<"openai-responses">` entries.
|
|
1128
|
+
*
|
|
1129
|
+
* Single source of truth for the curated to Model fan-in, consumed by both
|
|
1130
|
+
* - {@link xaiOAuthModelManagerOptions} (runtime static seed handed to the model
|
|
1131
|
+
* manager so the picker is populated on a fresh login), and
|
|
1132
|
+
* - \`packages/catalog/scripts/generate-models.ts\` (bundles the same entries into
|
|
1133
|
+
* `models.json`, so the synchronous `ModelRegistry.#loadModels()` boot path
|
|
1134
|
+
* sees `xai-oauth` without waiting for a refresh — fixes the boot-time
|
|
1135
|
+
* default-model reset when `modelRoles.default = "xai-oauth/<id>"`).
|
|
1136
|
+
*
|
|
1137
|
+
* `reasoning` defaults to `true` for the Grok-4.x family; the explicit
|
|
1138
|
+
* `grok-4.20-0309-non-reasoning` entry opts out via `XAICuratedModel.reasoning`.
|
|
1139
|
+
* `maxTokens` mirrors each model's `contextWindow` (the OAuth surface reports
|
|
1140
|
+
* no per-request output limit); the openai-responses wire still clamps the
|
|
1141
|
+
* actual request to OPENAI_MAX_OUTPUT_TOKENS. Mirrors
|
|
1142
|
+
* `hermes-agent/hermes_cli/models.py:_XAI_STATIC_FALLBACK`.
|
|
1143
|
+
*/
|
|
1144
|
+
export function buildXaiOAuthStaticSeed(baseUrl?: string): ModelSpec<"openai-responses">[] {
|
|
1145
|
+
const resolvedBaseUrl = baseUrl ?? "https://api.x.ai/v1";
|
|
1146
|
+
return XAI_OAUTH_CURATED_MODELS.map(curated => {
|
|
1147
|
+
// Synthesise a bare base then layer curated metadata via the same helper
|
|
1148
|
+
// the dynamic overlay/inject paths use. `name: curated.id` is a sentinel
|
|
1149
|
+
// the helper rewrites to `curated.name ?? base.name`, so curated.name
|
|
1150
|
+
// wins when set.
|
|
1151
|
+
const base: ModelSpec<"openai-responses"> = {
|
|
1152
|
+
id: curated.id,
|
|
1153
|
+
name: curated.id,
|
|
1154
|
+
api: "openai-responses",
|
|
1155
|
+
provider: "xai-oauth",
|
|
1156
|
+
baseUrl: resolvedBaseUrl,
|
|
1157
|
+
reasoning: true,
|
|
1158
|
+
input: ["text"],
|
|
1159
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
1160
|
+
contextWindow: curated.contextWindow,
|
|
1161
|
+
maxTokens: curated.contextWindow,
|
|
1162
|
+
compat: { reasoningEffortMap: XAI_REASONING_EFFORT_MAP },
|
|
1163
|
+
};
|
|
1164
|
+
return mergeCuratedIntoModel(base, curated);
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
export function xaiOAuthModelManagerOptions(
|
|
1169
|
+
config?: XaiOAuthModelManagerConfig,
|
|
1170
|
+
): ModelManagerOptions<"openai-responses"> {
|
|
1171
|
+
const defaultBaseUrl = "https://api.x.ai/v1";
|
|
1172
|
+
const resolvedBaseUrl = config?.baseUrl ?? defaultBaseUrl;
|
|
1173
|
+
const base = createSimpleOpenAIResponsesOptions(
|
|
1174
|
+
"xai-oauth" as Parameters<typeof getBundledModels>[0],
|
|
1175
|
+
defaultBaseUrl,
|
|
1176
|
+
config,
|
|
1177
|
+
);
|
|
1178
|
+
// Static seed handed to the runtime model manager so the picker populates on
|
|
1179
|
+
// a fresh login even before `fetchDynamicModels` fires (it is gated on
|
|
1180
|
+
// `config.apiKey` at construction time, and OAuth tokens resolve later via
|
|
1181
|
+
// AuthStorage). \`generate-models.ts\` calls the same builder so \`models.json\`
|
|
1182
|
+
// carries these entries too — making the synchronous `#loadModels()` boot
|
|
1183
|
+
// path honor `modelRoles.default = "xai-oauth/<id>"` without `await refresh()`.
|
|
1184
|
+
const staticModels = buildXaiOAuthStaticSeed(resolvedBaseUrl);
|
|
1185
|
+
if (!base.fetchDynamicModels) {
|
|
1186
|
+
return { ...base, staticModels };
|
|
1187
|
+
}
|
|
1188
|
+
// Wrap fetchDynamicModels so an `online refresh` against xAI's /v1/models
|
|
1189
|
+
// runs through applyXAIOAuthCuration — preserves curated context windows,
|
|
1190
|
+
// vision modality, reasoning flags, and filters tool-only model ids
|
|
1191
|
+
// (grok-imagine-*, grok-stt-*, grok-voice-*) from the chat picker.
|
|
1192
|
+
const inner = base.fetchDynamicModels;
|
|
1193
|
+
return {
|
|
1194
|
+
...base,
|
|
1195
|
+
staticModels,
|
|
1196
|
+
fetchDynamicModels: async () => {
|
|
1197
|
+
const dynamic = await inner();
|
|
1198
|
+
return dynamic == null ? dynamic : applyXAIOAuthCuration(dynamic);
|
|
1199
|
+
},
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// ---------------------------------------------------------------------------
|
|
1204
|
+
// 6.4 AIML API
|
|
1205
|
+
// ---------------------------------------------------------------------------
|
|
1206
|
+
|
|
1207
|
+
const AIML_API_NON_CHAT_MODEL_ID_PATTERN =
|
|
1208
|
+
/(?:^|[/:._-])(?:audio|embed|embedding|embeddings|i2i|i2v|image|speech|t2i|t2v|tts|video)(?:$|[/:._-])/i;
|
|
1209
|
+
|
|
1210
|
+
const AIML_API_NON_CHAT_MODEL_ID_SUBSTRINGS = ["dall-e", "dalle", "flux", "imagen", "sora", "veo", "whisper"] as const;
|
|
1211
|
+
|
|
1212
|
+
export function isLikelyAimlApiChatModelId(id: string): boolean {
|
|
1213
|
+
const normalized = id.trim().toLowerCase();
|
|
1214
|
+
if (!normalized) return false;
|
|
1215
|
+
return (
|
|
1216
|
+
!AIML_API_NON_CHAT_MODEL_ID_PATTERN.test(normalized) &&
|
|
1217
|
+
!AIML_API_NON_CHAT_MODEL_ID_SUBSTRINGS.some(token => normalized.includes(token))
|
|
1218
|
+
);
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
export interface AimlApiModelManagerConfig {
|
|
1222
|
+
apiKey?: string;
|
|
1223
|
+
baseUrl?: string;
|
|
1224
|
+
fetch?: FetchImpl;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
export function aimlApiModelManagerOptions(
|
|
1228
|
+
config?: AimlApiModelManagerConfig,
|
|
1229
|
+
): ModelManagerOptions<"openai-completions"> {
|
|
1230
|
+
const apiKey = config?.apiKey;
|
|
1231
|
+
const baseUrl = config?.baseUrl ?? "https://api.aimlapi.com/v1";
|
|
1232
|
+
const references = createBundledReferenceMap<"openai-completions">("aimlapi");
|
|
1233
|
+
return {
|
|
1234
|
+
providerId: "aimlapi",
|
|
1235
|
+
dynamicModelsAuthoritative: true,
|
|
1236
|
+
...(apiKey && {
|
|
1237
|
+
fetchDynamicModels: () =>
|
|
1238
|
+
fetchOpenAICompatibleModels({
|
|
1239
|
+
api: "openai-completions",
|
|
1240
|
+
provider: "aimlapi",
|
|
1241
|
+
baseUrl,
|
|
1242
|
+
apiKey,
|
|
1243
|
+
filterModel: (_entry, model) => isLikelyAimlApiChatModelId(model.id),
|
|
1244
|
+
mapModel: (entry, defaults) => {
|
|
1245
|
+
const reference = references.get(defaults.id);
|
|
1246
|
+
return mapWithBundledReference(entry, defaults, reference);
|
|
1247
|
+
},
|
|
1248
|
+
fetch: config?.fetch,
|
|
1249
|
+
}),
|
|
1250
|
+
}),
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
// ---------------------------------------------------------------------------
|
|
1255
|
+
// 6.5 DeepSeek
|
|
1256
|
+
// ---------------------------------------------------------------------------
|
|
1257
|
+
|
|
1258
|
+
export interface DeepSeekModelManagerConfig {
|
|
1259
|
+
apiKey?: string;
|
|
1260
|
+
baseUrl?: string;
|
|
1261
|
+
fetch?: FetchImpl;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
export function deepseekModelManagerOptions(
|
|
1265
|
+
config?: DeepSeekModelManagerConfig,
|
|
1266
|
+
): ModelManagerOptions<"openai-completions"> {
|
|
1267
|
+
return createSimpleOpenAICompletionsOptions("deepseek", "https://api.deepseek.com", config);
|
|
1268
|
+
}
|
|
1269
|
+
// ---------------------------------------------------------------------------
|
|
1270
|
+
// 6.7 Zhipu Coding Plan
|
|
1271
|
+
// ---------------------------------------------------------------------------
|
|
1272
|
+
|
|
1273
|
+
export interface ZhipuCodingPlanModelManagerConfig {
|
|
1274
|
+
apiKey?: string;
|
|
1275
|
+
baseUrl?: string;
|
|
1276
|
+
fetch?: FetchImpl;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
export function zhipuCodingPlanModelManagerOptions(
|
|
1280
|
+
config?: ZhipuCodingPlanModelManagerConfig,
|
|
1281
|
+
): ModelManagerOptions<"openai-completions"> {
|
|
1282
|
+
const apiKey = config?.apiKey;
|
|
1283
|
+
const baseUrl = config?.baseUrl ?? "https://open.bigmodel.cn/api/coding/paas/v4";
|
|
1284
|
+
return {
|
|
1285
|
+
providerId: "zhipu-coding-plan",
|
|
1286
|
+
...(apiKey && {
|
|
1287
|
+
fetchDynamicModels: () =>
|
|
1288
|
+
fetchOpenAICompatibleModels({
|
|
1289
|
+
api: "openai-completions",
|
|
1290
|
+
provider: "zhipu-coding-plan",
|
|
1291
|
+
baseUrl,
|
|
1292
|
+
apiKey,
|
|
1293
|
+
mapModel: (
|
|
1294
|
+
_entry: OpenAICompatibleModelRecord,
|
|
1295
|
+
defaults: ModelSpec<"openai-completions">,
|
|
1296
|
+
_context: OpenAICompatibleModelMapperContext<"openai-completions">,
|
|
1297
|
+
): ModelSpec<"openai-completions"> => {
|
|
1298
|
+
const id = defaults.id;
|
|
1299
|
+
return {
|
|
1300
|
+
...defaults,
|
|
1301
|
+
reasoning: isReasoningGlmModelId(id) || id.includes("thinking"),
|
|
1302
|
+
input: isGlmVisionModelId(id) ? (["text", "image"] as const) : ["text"],
|
|
1303
|
+
compat: {
|
|
1304
|
+
thinkingFormat: "zai",
|
|
1305
|
+
reasoningContentField: "reasoning_content",
|
|
1306
|
+
supportsDeveloperRole: false,
|
|
1307
|
+
},
|
|
1308
|
+
};
|
|
1309
|
+
},
|
|
1310
|
+
fetch: config?.fetch,
|
|
1311
|
+
}),
|
|
1312
|
+
}),
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
// ---------------------------------------------------------------------------
|
|
1317
|
+
// 7.5 Fireworks
|
|
1318
|
+
// ---------------------------------------------------------------------------
|
|
1319
|
+
|
|
1320
|
+
/**
|
|
1321
|
+
* Fireworks-published cap for the Kimi K2 family. Fireworks' `/v1/models`
|
|
1322
|
+
* envelope generically reports `max_completion_tokens: 65536` for every Kimi
|
|
1323
|
+
* deployment, but Kimi K2 (instruct / thinking / turbo) on Fireworks is
|
|
1324
|
+
* documented to ship long reasoning traces that should be bounded — capping
|
|
1325
|
+
* at 32,768 prevents handing callers a budget the router cannot honor.
|
|
1326
|
+
* See https://github.com/can1357/oh-my-pi/issues/1849.
|
|
1327
|
+
*/
|
|
1328
|
+
export const FIREWORKS_KIMI_MAX_TOKENS = 32_768;
|
|
1329
|
+
|
|
1330
|
+
/**
|
|
1331
|
+
* Returns true for any Kimi K2.x public model id served by Fireworks-backed
|
|
1332
|
+
* providers (`fireworks` direct, `firepass` router). Matches both the public
|
|
1333
|
+
* catalog id (`kimi-k2.5`, `kimi-k2.6`, `kimi-k2.6-turbo`) and the canonical
|
|
1334
|
+
* Fireworks wire id (`accounts/fireworks/{models,routers}/kimi-k2…`).
|
|
1335
|
+
*/
|
|
1336
|
+
export function isFireworksKimiK2ModelId(modelId: string): boolean {
|
|
1337
|
+
const trimmed = modelId.toLowerCase();
|
|
1338
|
+
if (trimmed.startsWith("kimi-k2")) return true;
|
|
1339
|
+
return /\/kimi-k2(?:p\d+)?(?:[._-]|$)/.test(trimmed);
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
/**
|
|
1343
|
+
* Clamp the Kimi K2 family's `maxTokens` to {@link FIREWORKS_KIMI_MAX_TOKENS}
|
|
1344
|
+
* on Fireworks-backed providers, leaving every other model untouched.
|
|
1345
|
+
*/
|
|
1346
|
+
export function clampFireworksKimiMaxTokens(modelId: string, candidate: number): number;
|
|
1347
|
+
export function clampFireworksKimiMaxTokens(modelId: string, candidate: number | null): number | null;
|
|
1348
|
+
export function clampFireworksKimiMaxTokens(modelId: string, candidate: number | null): number | null {
|
|
1349
|
+
if (candidate === null) return null;
|
|
1350
|
+
return isFireworksKimiK2ModelId(modelId) ? Math.min(candidate, FIREWORKS_KIMI_MAX_TOKENS) : candidate;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
/**
|
|
1354
|
+
* Kimi K2.7 Code's documented recommended output budget. Some provider
|
|
1355
|
+
* discovery rows report the context-sized `max_completion_tokens` instead.
|
|
1356
|
+
*/
|
|
1357
|
+
export const KIMI_K27_CODE_RECOMMENDED_MAX_TOKENS = 32_768;
|
|
1358
|
+
|
|
1359
|
+
export function isKimiK27CodeModelId(modelId: string): boolean {
|
|
1360
|
+
return /(?:^|\/)kimi[-._]?k2(?:[._-]?|p)7[-._]?code(?:[-._]?highspeed)?$/i.test(modelId);
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
export function clampKimiK27CodeMaxTokens(modelId: string, candidate: number): number;
|
|
1364
|
+
export function clampKimiK27CodeMaxTokens(modelId: string, candidate: number | null): number | null;
|
|
1365
|
+
export function clampKimiK27CodeMaxTokens(modelId: string, candidate: number | null): number | null {
|
|
1366
|
+
if (candidate === null) return null;
|
|
1367
|
+
return isKimiK27CodeModelId(modelId) ? Math.min(candidate, KIMI_K27_CODE_RECOMMENDED_MAX_TOKENS) : candidate;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
/**
|
|
1371
|
+
* Fireworks Fast variants we surface. Each inherits the base model's
|
|
1372
|
+
* limits/modalities/thinking and overrides only the cost with the Standard-column
|
|
1373
|
+
* Fast prices from the Serverless pricing table; `cacheWrite` stays 0 (Fireworks
|
|
1374
|
+
* bills no cache-write). Derived from the bundled base entries so metadata stays
|
|
1375
|
+
* in lockstep, and the runtime auto-falls back to the base id on a failed fast
|
|
1376
|
+
* request. See https://docs.fireworks.ai/serverless/pricing.
|
|
1377
|
+
*/
|
|
1378
|
+
const FIREWORKS_FAST_VARIANT_SPECS: ReadonlyArray<{
|
|
1379
|
+
base: string;
|
|
1380
|
+
name: string;
|
|
1381
|
+
cost: { input: number; output: number; cacheRead: number };
|
|
1382
|
+
}> = [
|
|
1383
|
+
{ base: "kimi-k2.7-code", name: "Kimi K2.7 Code Fast", cost: { input: 1.9, output: 8, cacheRead: 0.38 } },
|
|
1384
|
+
{ base: "kimi-k2.6", name: "Kimi K2.6 Fast", cost: { input: 2, output: 8, cacheRead: 0.3 } },
|
|
1385
|
+
{ base: "glm-5.1", name: "GLM-5.1 Fast", cost: { input: 2.8, output: 8.8, cacheRead: 0.52 } },
|
|
1386
|
+
];
|
|
1387
|
+
|
|
1388
|
+
/**
|
|
1389
|
+
* Build the Fireworks Fast seed by projecting each base bundled spec into a
|
|
1390
|
+
* `<id>-fast` variant. Pushed into the generated catalog (Fast routers never
|
|
1391
|
+
* appear in the serverless control-plane list, so discovery cannot surface
|
|
1392
|
+
* them) and deduped behind any identical previous-snapshot entry.
|
|
1393
|
+
*/
|
|
1394
|
+
export function buildFireworksFastSeed(): ModelSpec<"openai-completions">[] {
|
|
1395
|
+
const bundled = createBundledReferenceMap<"openai-completions">("fireworks");
|
|
1396
|
+
const seeds: ModelSpec<"openai-completions">[] = [];
|
|
1397
|
+
for (const variant of FIREWORKS_FAST_VARIANT_SPECS) {
|
|
1398
|
+
const base = bundled.get(variant.base);
|
|
1399
|
+
if (!base) continue;
|
|
1400
|
+
seeds.push({
|
|
1401
|
+
...base,
|
|
1402
|
+
id: `${variant.base}${FIREWORKS_FAST_SUFFIX}`,
|
|
1403
|
+
name: variant.name,
|
|
1404
|
+
cost: {
|
|
1405
|
+
input: variant.cost.input,
|
|
1406
|
+
output: variant.cost.output,
|
|
1407
|
+
cacheRead: variant.cost.cacheRead,
|
|
1408
|
+
cacheWrite: 0,
|
|
1409
|
+
},
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
return seeds;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
/**
|
|
1416
|
+
* Fireworks DeepSeek V4 accepts effort via `reasoning_effort` but rejects the
|
|
1417
|
+
* DeepSeek-native binary `thinking` toggle when both are present.
|
|
1418
|
+
*/
|
|
1419
|
+
export function stripFireworksDeepSeekThinkingToggle(
|
|
1420
|
+
model: ModelSpec<"openai-completions">,
|
|
1421
|
+
publicModelId: string,
|
|
1422
|
+
): ModelSpec<"openai-completions"> {
|
|
1423
|
+
if (!publicModelId.startsWith("deepseek-v4")) return model;
|
|
1424
|
+
const compat = model.compat;
|
|
1425
|
+
if (!compat?.extraBody || !("thinking" in compat.extraBody)) return model;
|
|
1426
|
+
|
|
1427
|
+
const extraBody = { ...compat.extraBody };
|
|
1428
|
+
delete extraBody.thinking;
|
|
1429
|
+
if (Object.keys(extraBody).length > 0) {
|
|
1430
|
+
return { ...model, compat: { ...compat, extraBody } };
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
const nextCompat = { ...compat };
|
|
1434
|
+
delete nextCompat.extraBody;
|
|
1435
|
+
return { ...model, compat: nextCompat };
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
export interface FireworksModelManagerConfig {
|
|
1439
|
+
apiKey?: string;
|
|
1440
|
+
baseUrl?: string;
|
|
1441
|
+
fetch?: FetchImpl;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
const FIREWORKS_CONTROL_PLANE_ACCOUNT = "fireworks";
|
|
1445
|
+
const FIREWORKS_SERVERLESS_FILTER = "supports_serverless=true";
|
|
1446
|
+
const FIREWORKS_CONTROL_PLANE_PAGE_SIZE = 200;
|
|
1447
|
+
const FIREWORKS_CONTROL_PLANE_MAX_PAGES = 25;
|
|
1448
|
+
|
|
1449
|
+
/**
|
|
1450
|
+
* One record from the Fireworks control-plane catalog
|
|
1451
|
+
* (`GET /v1/accounts/{account}/models`). This is distinct from the
|
|
1452
|
+
* OpenAI-compatible `/v1/models` inference envelope: the control plane
|
|
1453
|
+
* enumerates the full serverless catalog with camelCase capability metadata,
|
|
1454
|
+
* including on-demand models (e.g. `kimi-k2p7-code`) that never surface in
|
|
1455
|
+
* `/v1/models`. Discovering here is what keeps new serverless models appearing
|
|
1456
|
+
* without catalog edits — see the Fireworks docs `List Models` API.
|
|
1457
|
+
*/
|
|
1458
|
+
interface FireworksControlPlaneModel {
|
|
1459
|
+
/** Resource name, e.g. `accounts/fireworks/models/kimi-k2p7-code`. */
|
|
1460
|
+
name?: unknown;
|
|
1461
|
+
displayName?: unknown;
|
|
1462
|
+
contextLength?: unknown;
|
|
1463
|
+
supportsImageInput?: unknown;
|
|
1464
|
+
supportsTools?: unknown;
|
|
1465
|
+
supportsServerless?: unknown;
|
|
1466
|
+
state?: unknown;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
/**
|
|
1470
|
+
* Derive the control-plane list endpoint from the inference base URL. The
|
|
1471
|
+
* inference API lives under `/inference/v1` while the control plane is
|
|
1472
|
+
* `/v1/accounts/<account>/models` on the same origin, so we route off origin.
|
|
1473
|
+
* Returns null for unparseable overrides (custom gateways) so discovery falls
|
|
1474
|
+
* back to the cached/bundled catalog.
|
|
1475
|
+
*/
|
|
1476
|
+
function toFireworksControlPlaneModelsUrl(baseUrl: string, account: string): string | null {
|
|
1477
|
+
try {
|
|
1478
|
+
return `${new URL(baseUrl).origin}/v1/accounts/${account}/models`;
|
|
1479
|
+
} catch {
|
|
1480
|
+
return null;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
function mapFireworksControlPlaneModel(
|
|
1485
|
+
record: FireworksControlPlaneModel,
|
|
1486
|
+
publicModelId: string,
|
|
1487
|
+
reference: ModelSpec<"openai-completions"> | undefined,
|
|
1488
|
+
baseUrl: string,
|
|
1489
|
+
): ModelSpec<"openai-completions"> {
|
|
1490
|
+
const name = toModelName(record.displayName, reference?.name ?? publicModelId);
|
|
1491
|
+
const supportsImage = toBoolean(record.supportsImageInput) === true;
|
|
1492
|
+
const supportsTools = toBoolean(record.supportsTools);
|
|
1493
|
+
const contextWindow = toPositiveNumber(record.contextLength, reference?.contextWindow ?? null);
|
|
1494
|
+
// The control plane reports no max-output budget; default the Kimi family to
|
|
1495
|
+
// its published cap, everyone else to the discovery fallback, then clamp.
|
|
1496
|
+
const fallbackMaxTokens = isFireworksKimiK2ModelId(publicModelId) ? FIREWORKS_KIMI_MAX_TOKENS : null;
|
|
1497
|
+
const maxTokens = clampFireworksKimiMaxTokens(publicModelId, reference?.maxTokens ?? fallbackMaxTokens);
|
|
1498
|
+
const base: ModelSpec<"openai-completions"> = reference ?? {
|
|
1499
|
+
id: publicModelId,
|
|
1500
|
+
name,
|
|
1501
|
+
api: "openai-completions",
|
|
1502
|
+
provider: "fireworks",
|
|
1503
|
+
baseUrl,
|
|
1504
|
+
reasoning: false,
|
|
1505
|
+
input: ["text"],
|
|
1506
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
1507
|
+
contextWindow,
|
|
1508
|
+
maxTokens,
|
|
1509
|
+
};
|
|
1510
|
+
const model: ModelSpec<"openai-completions"> = {
|
|
1511
|
+
...base,
|
|
1512
|
+
id: publicModelId,
|
|
1513
|
+
api: "openai-completions",
|
|
1514
|
+
provider: "fireworks",
|
|
1515
|
+
baseUrl,
|
|
1516
|
+
name,
|
|
1517
|
+
// The control plane exposes capability flags but no reasoning bit. Every
|
|
1518
|
+
// serverless chat LLM Fireworks ships reasons, and `buildModel` derives
|
|
1519
|
+
// the Fireworks effort map from the id at build time — so default
|
|
1520
|
+
// unbundled models to reasoning while bundled references keep their value.
|
|
1521
|
+
reasoning: reference?.reasoning ?? true,
|
|
1522
|
+
input: supportsImage ? ["text", "image"] : (reference?.input ?? ["text"]),
|
|
1523
|
+
contextWindow,
|
|
1524
|
+
maxTokens,
|
|
1525
|
+
...(supportsTools === false ? { supportsTools: false } : {}),
|
|
1526
|
+
};
|
|
1527
|
+
return stripFireworksDeepSeekThinkingToggle(model, publicModelId);
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
/**
|
|
1531
|
+
* Discover Fireworks serverless models via the control-plane `List Models`
|
|
1532
|
+
* API (`supports_serverless=true`), paginating the full catalog. Returns null
|
|
1533
|
+
* on any transport/protocol failure so the model manager keeps the cached or
|
|
1534
|
+
* bundled catalog rather than caching a truncated list as authoritative.
|
|
1535
|
+
*/
|
|
1536
|
+
async function fetchFireworksServerlessModels(options: {
|
|
1537
|
+
baseUrl: string;
|
|
1538
|
+
apiKey: string;
|
|
1539
|
+
resolveReference: (publicModelId: string) => ModelSpec<"openai-completions"> | undefined;
|
|
1540
|
+
fetch?: FetchImpl;
|
|
1541
|
+
}): Promise<ModelSpec<"openai-completions">[] | null> {
|
|
1542
|
+
const listUrl = toFireworksControlPlaneModelsUrl(options.baseUrl, FIREWORKS_CONTROL_PLANE_ACCOUNT);
|
|
1543
|
+
if (!listUrl) return null;
|
|
1544
|
+
const fetchImpl = discoveryFetch(options.fetch);
|
|
1545
|
+
const collected = new Map<string, ModelSpec<"openai-completions">>();
|
|
1546
|
+
let pageToken = "";
|
|
1547
|
+
for (let page = 0; page < FIREWORKS_CONTROL_PLANE_MAX_PAGES; page++) {
|
|
1548
|
+
const url = new URL(listUrl);
|
|
1549
|
+
url.searchParams.set("filter", FIREWORKS_SERVERLESS_FILTER);
|
|
1550
|
+
url.searchParams.set("pageSize", String(FIREWORKS_CONTROL_PLANE_PAGE_SIZE));
|
|
1551
|
+
if (pageToken) url.searchParams.set("pageToken", pageToken);
|
|
1552
|
+
let response: Response;
|
|
1553
|
+
try {
|
|
1554
|
+
response = await fetchImpl(url.toString(), {
|
|
1555
|
+
method: "GET",
|
|
1556
|
+
headers: { Accept: "application/json", Authorization: `Bearer ${options.apiKey}` },
|
|
1557
|
+
});
|
|
1558
|
+
} catch {
|
|
1559
|
+
return null;
|
|
1560
|
+
}
|
|
1561
|
+
if (!response.ok) return null;
|
|
1562
|
+
let payload: unknown;
|
|
1563
|
+
try {
|
|
1564
|
+
payload = await response.json();
|
|
1565
|
+
} catch {
|
|
1566
|
+
return null;
|
|
1567
|
+
}
|
|
1568
|
+
if (!isRecord(payload)) return null;
|
|
1569
|
+
const models = Array.isArray(payload.models) ? payload.models : [];
|
|
1570
|
+
for (const entry of models) {
|
|
1571
|
+
if (!isRecord(entry)) continue;
|
|
1572
|
+
const record = entry as FireworksControlPlaneModel;
|
|
1573
|
+
if (toBoolean(record.supportsServerless) !== true) continue;
|
|
1574
|
+
if (typeof record.state === "string" && record.state !== "READY") continue;
|
|
1575
|
+
const wireName = typeof record.name === "string" ? record.name : "";
|
|
1576
|
+
if (!wireName) continue;
|
|
1577
|
+
const publicModelId = toFireworksPublicModelId(wireName);
|
|
1578
|
+
if (!publicModelId) continue;
|
|
1579
|
+
collected.set(
|
|
1580
|
+
publicModelId,
|
|
1581
|
+
mapFireworksControlPlaneModel(
|
|
1582
|
+
record,
|
|
1583
|
+
publicModelId,
|
|
1584
|
+
options.resolveReference(publicModelId),
|
|
1585
|
+
options.baseUrl,
|
|
1586
|
+
),
|
|
1587
|
+
);
|
|
1588
|
+
}
|
|
1589
|
+
const next = typeof payload.nextPageToken === "string" ? payload.nextPageToken : "";
|
|
1590
|
+
if (!next) break;
|
|
1591
|
+
pageToken = next;
|
|
1592
|
+
}
|
|
1593
|
+
return Array.from(collected.values());
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
function createModelsDevReferenceMap<TApi extends Api>(
|
|
1597
|
+
models: readonly ModelSpec<Api>[],
|
|
1598
|
+
): Map<string, ModelSpec<TApi>> {
|
|
1599
|
+
const references = new Map<string, ModelSpec<TApi>>();
|
|
1600
|
+
for (const model of models) {
|
|
1601
|
+
const candidate = model as ModelSpec<TApi>;
|
|
1602
|
+
const existing = references.get(candidate.id);
|
|
1603
|
+
if (!existing) {
|
|
1604
|
+
references.set(candidate.id, candidate);
|
|
1605
|
+
continue;
|
|
1606
|
+
}
|
|
1607
|
+
if ((candidate.contextWindow ?? 0) > (existing.contextWindow ?? 0)) {
|
|
1608
|
+
references.set(candidate.id, candidate);
|
|
1609
|
+
continue;
|
|
1610
|
+
}
|
|
1611
|
+
if (
|
|
1612
|
+
candidate.contextWindow === existing.contextWindow &&
|
|
1613
|
+
(candidate.maxTokens ?? 0) > (existing.maxTokens ?? 0)
|
|
1614
|
+
) {
|
|
1615
|
+
references.set(candidate.id, candidate);
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
return references;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
async function loadModelsDevReferences<TApi extends Api>(fetchImpl?: FetchImpl): Promise<Map<string, ModelSpec<TApi>>> {
|
|
1622
|
+
try {
|
|
1623
|
+
const payload = await fetchModelsDevPayload(fetchImpl);
|
|
1624
|
+
return createModelsDevReferenceMap<TApi>(
|
|
1625
|
+
mapModelsDevToModels(payload as Record<string, unknown>, MODELS_DEV_PROVIDER_DESCRIPTORS),
|
|
1626
|
+
);
|
|
1627
|
+
} catch {
|
|
1628
|
+
return new Map<string, ModelSpec<TApi>>();
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
export function fireworksModelManagerOptions(
|
|
1632
|
+
config?: FireworksModelManagerConfig,
|
|
1633
|
+
): ModelManagerOptions<"openai-completions"> {
|
|
1634
|
+
const apiKey = config?.apiKey;
|
|
1635
|
+
const baseUrl = config?.baseUrl ?? "https://api.fireworks.ai/inference/v1";
|
|
1636
|
+
const bundledReferences = createReferenceResolver(createBundledReferenceMap<"openai-completions">("fireworks"));
|
|
1637
|
+
return {
|
|
1638
|
+
providerId: "fireworks",
|
|
1639
|
+
...(apiKey && {
|
|
1640
|
+
fetchDynamicModels: async () => {
|
|
1641
|
+
const modelsDevReferences = await loadModelsDevReferences<"openai-completions">(config?.fetch);
|
|
1642
|
+
return fetchFireworksServerlessModels({
|
|
1643
|
+
baseUrl,
|
|
1644
|
+
apiKey,
|
|
1645
|
+
resolveReference: publicModelId =>
|
|
1646
|
+
modelsDevReferences.get(publicModelId) ?? bundledReferences(publicModelId),
|
|
1647
|
+
fetch: config?.fetch,
|
|
1648
|
+
});
|
|
1649
|
+
},
|
|
1650
|
+
}),
|
|
1651
|
+
};
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
// ---------------------------------------------------------------------------
|
|
1655
|
+
// 7.6 Fire Pass (Fireworks Kimi K2.6 Turbo subscription)
|
|
1656
|
+
// ---------------------------------------------------------------------------
|
|
1657
|
+
|
|
1658
|
+
export interface FirepassModelManagerConfig {
|
|
1659
|
+
apiKey?: string;
|
|
1660
|
+
baseUrl?: string;
|
|
1661
|
+
fetch?: FetchImpl;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
/**
|
|
1665
|
+
* Fire Pass is a Fireworks subscription product that exposes a single router
|
|
1666
|
+
* model (Kimi K2.6 Turbo) under `accounts/fireworks/routers/kimi-k2p6-turbo`.
|
|
1667
|
+
* The dedicated `fpk_…` keys do not authorize `/v1/models`, so this manager
|
|
1668
|
+
* never performs dynamic discovery — the bundled catalog entry is canonical.
|
|
1669
|
+
* See https://docs.fireworks.ai/firepass.
|
|
1670
|
+
*/
|
|
1671
|
+
export function firepassModelManagerOptions(
|
|
1672
|
+
_config?: FirepassModelManagerConfig,
|
|
1673
|
+
): ModelManagerOptions<"openai-completions"> {
|
|
1674
|
+
return {
|
|
1675
|
+
providerId: "firepass",
|
|
1676
|
+
};
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
// ---------------------------------------------------------------------------
|
|
1680
|
+
// 7.7 Wafer Serverless
|
|
1681
|
+
// ---------------------------------------------------------------------------
|
|
1682
|
+
|
|
1683
|
+
export interface WaferModelManagerConfig {
|
|
1684
|
+
apiKey?: string;
|
|
1685
|
+
baseUrl?: string;
|
|
1686
|
+
fetch?: FetchImpl;
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
const WAFER_DEFAULT_BASE_URL = "https://pass.wafer.ai/v1";
|
|
1690
|
+
const WAFER_MAX_TOKENS_CAP = 65536;
|
|
1691
|
+
|
|
1692
|
+
/**
|
|
1693
|
+
* Mapper for Wafer Serverless `/v1/models` records.
|
|
1694
|
+
*
|
|
1695
|
+
* Wafer wraps each entry with a `wafer` envelope describing capabilities and
|
|
1696
|
+
* pricing. The mapper folds that metadata into the canonical
|
|
1697
|
+
* `ModelSpec<"openai-completions">` shape and applies upstream-specific thinking
|
|
1698
|
+
* compat when the entry advertises reasoning support. Wafer pricing is exposed
|
|
1699
|
+
* through internal wholesale units; the public Serverless rate equals
|
|
1700
|
+
* `cents × 125 / 10000`.
|
|
1701
|
+
*/
|
|
1702
|
+
interface WaferRecord {
|
|
1703
|
+
context_length?: unknown;
|
|
1704
|
+
tier?: unknown;
|
|
1705
|
+
provider?: unknown;
|
|
1706
|
+
capabilities?: { vision?: unknown; reasoning?: unknown; tools?: unknown };
|
|
1707
|
+
pricing?: {
|
|
1708
|
+
input_cents_per_million?: unknown;
|
|
1709
|
+
output_cents_per_million?: unknown;
|
|
1710
|
+
cache_read_cents_per_million?: unknown;
|
|
1711
|
+
};
|
|
1712
|
+
display_name?: unknown;
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
function readWaferRecord(entry: OpenAICompatibleModelRecord): WaferRecord | undefined {
|
|
1716
|
+
const raw = (entry as { wafer?: unknown }).wafer;
|
|
1717
|
+
return raw && typeof raw === "object" ? (raw as WaferRecord) : undefined;
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
function mapWaferModel(
|
|
1721
|
+
providerId: "wafer-serverless",
|
|
1722
|
+
baseUrl: string,
|
|
1723
|
+
entry: OpenAICompatibleModelRecord,
|
|
1724
|
+
defaults: ModelSpec<"openai-completions">,
|
|
1725
|
+
): ModelSpec<"openai-completions"> {
|
|
1726
|
+
const wafer = readWaferRecord(entry);
|
|
1727
|
+
const capabilities = wafer?.capabilities ?? {};
|
|
1728
|
+
const reasoning = capabilities.reasoning === true;
|
|
1729
|
+
const vision = capabilities.vision === true;
|
|
1730
|
+
const supportsTools = toBoolean(capabilities.tools) === false ? false : undefined;
|
|
1731
|
+
const contextWindow = toPositiveNumber(
|
|
1732
|
+
wafer?.context_length,
|
|
1733
|
+
toPositiveNumber((entry as { max_model_len?: unknown }).max_model_len, defaults.contextWindow),
|
|
1734
|
+
);
|
|
1735
|
+
const maxTokens = contextWindow !== null ? Math.min(contextWindow, WAFER_MAX_TOKENS_CAP) : null;
|
|
1736
|
+
const pricing = wafer?.pricing ?? {};
|
|
1737
|
+
const cost = {
|
|
1738
|
+
input: (toPositiveNumber(pricing.input_cents_per_million, 0) * 125) / 10000,
|
|
1739
|
+
output: (toPositiveNumber(pricing.output_cents_per_million, 0) * 125) / 10000,
|
|
1740
|
+
cacheRead: (toPositiveNumber(pricing.cache_read_cents_per_million, 0) * 125) / 10000,
|
|
1741
|
+
cacheWrite: 0,
|
|
1742
|
+
};
|
|
1743
|
+
const name = toModelName(wafer?.display_name, defaults.name);
|
|
1744
|
+
const base: ModelSpec<"openai-completions"> = {
|
|
1745
|
+
...defaults,
|
|
1746
|
+
id: defaults.id,
|
|
1747
|
+
name,
|
|
1748
|
+
api: "openai-completions",
|
|
1749
|
+
provider: providerId,
|
|
1750
|
+
baseUrl,
|
|
1751
|
+
reasoning,
|
|
1752
|
+
input: vision ? (["text", "image"] as const) : ["text"],
|
|
1753
|
+
cost,
|
|
1754
|
+
contextWindow,
|
|
1755
|
+
maxTokens,
|
|
1756
|
+
...(supportsTools === false ? { supportsTools } : {}),
|
|
1757
|
+
};
|
|
1758
|
+
if (reasoning) {
|
|
1759
|
+
// Wafer's `wafer.provider` envelope tells us which upstream backend serves
|
|
1760
|
+
// the model. Each upstream accepts a different thinking-control parameter
|
|
1761
|
+
// on the wire — Wafer passes the body through, so we must mirror the
|
|
1762
|
+
// upstream's native shape:
|
|
1763
|
+
// - zai (GLM) and moonshotai (Kimi) → `thinking: { type: "enabled" | "disabled" }`
|
|
1764
|
+
// - qwen (Alibaba) → top-level `enable_thinking: boolean`
|
|
1765
|
+
// - deepseek → `reasoning_effort` (DeepSeek effort map; the model always
|
|
1766
|
+
// reasons when invoked, replay of `reasoning_content` is required on
|
|
1767
|
+
// tool-call turns — both handled by `detectOpenAICompat` from the id).
|
|
1768
|
+
// For unknown upstreams we omit `thinkingFormat` and let the per-id
|
|
1769
|
+
// detection in `detectOpenAICompat` pick a safe default.
|
|
1770
|
+
const upstream = typeof wafer?.provider === "string" ? wafer.provider : undefined;
|
|
1771
|
+
const thinkingFormat: "zai" | "qwen" | undefined =
|
|
1772
|
+
upstream === "zai" || upstream === "moonshotai" ? "zai" : upstream === "qwen" ? "qwen" : undefined;
|
|
1773
|
+
return {
|
|
1774
|
+
...base,
|
|
1775
|
+
compat: {
|
|
1776
|
+
...(thinkingFormat ? { thinkingFormat } : {}),
|
|
1777
|
+
reasoningContentField: "reasoning_content",
|
|
1778
|
+
supportsDeveloperRole: false,
|
|
1779
|
+
},
|
|
1780
|
+
};
|
|
1781
|
+
}
|
|
1782
|
+
return {
|
|
1783
|
+
...base,
|
|
1784
|
+
compat: { supportsDeveloperRole: false },
|
|
1785
|
+
};
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
export function waferServerlessModelManagerOptions(
|
|
1789
|
+
config?: WaferModelManagerConfig,
|
|
1790
|
+
): ModelManagerOptions<"openai-completions"> {
|
|
1791
|
+
const apiKey = config?.apiKey;
|
|
1792
|
+
const baseUrl = config?.baseUrl ?? WAFER_DEFAULT_BASE_URL;
|
|
1793
|
+
const providerId = "wafer-serverless" as const;
|
|
1794
|
+
return {
|
|
1795
|
+
providerId,
|
|
1796
|
+
...(apiKey && {
|
|
1797
|
+
fetchDynamicModels: () =>
|
|
1798
|
+
fetchOpenAICompatibleModels({
|
|
1799
|
+
api: "openai-completions",
|
|
1800
|
+
provider: providerId,
|
|
1801
|
+
baseUrl,
|
|
1802
|
+
apiKey,
|
|
1803
|
+
mapModel: (entry, defaults) => mapWaferModel(providerId, baseUrl, entry, defaults),
|
|
1804
|
+
fetch: config?.fetch,
|
|
1805
|
+
}),
|
|
1806
|
+
}),
|
|
1807
|
+
};
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
// ---------------------------------------------------------------------------
|
|
1811
|
+
// 7. Mistral
|
|
1812
|
+
// ---------------------------------------------------------------------------
|
|
1813
|
+
|
|
1814
|
+
export interface MistralModelManagerConfig {
|
|
1815
|
+
apiKey?: string;
|
|
1816
|
+
baseUrl?: string;
|
|
1817
|
+
fetch?: FetchImpl;
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
export function mistralModelManagerOptions(
|
|
1821
|
+
config?: MistralModelManagerConfig,
|
|
1822
|
+
): ModelManagerOptions<"openai-completions"> {
|
|
1823
|
+
return createSimpleOpenAICompletionsOptions("mistral", "https://api.mistral.ai/v1", config);
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
// ---------------------------------------------------------------------------
|
|
1827
|
+
// 8. OpenCode
|
|
1828
|
+
// ---------------------------------------------------------------------------
|
|
1829
|
+
|
|
1830
|
+
export interface OpenCodeModelManagerConfig {
|
|
1831
|
+
apiKey?: string;
|
|
1832
|
+
baseUrl?: string;
|
|
1833
|
+
fetch?: FetchImpl;
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
function normalizeOpenCodeBasePath(baseUrl: string | undefined, fallbackBasePath: string): string {
|
|
1837
|
+
const value = normalizeAnthropicBaseUrl(baseUrl, fallbackBasePath);
|
|
1838
|
+
return value.endsWith("/v1") ? value.slice(0, -3) : value;
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
function openCodeBaseUrlForApi(api: Api, basePath: string): string {
|
|
1842
|
+
return api === "anthropic-messages" ? basePath : `${basePath}/v1`;
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
function openCodeModelManagerOptions(
|
|
1846
|
+
providerId: "opencode-go" | "opencode-zen",
|
|
1847
|
+
defaultBasePath: string,
|
|
1848
|
+
config?: OpenCodeModelManagerConfig,
|
|
1849
|
+
): ModelManagerOptions<Api> {
|
|
1850
|
+
const apiKey = config?.apiKey;
|
|
1851
|
+
const basePath = normalizeOpenCodeBasePath(config?.baseUrl, defaultBasePath);
|
|
1852
|
+
const discoveryBaseUrl = openCodeBaseUrlForApi("openai-completions", basePath);
|
|
1853
|
+
const references = createBundledReferenceMap<Api>(providerId);
|
|
1854
|
+
return {
|
|
1855
|
+
providerId,
|
|
1856
|
+
...(apiKey && {
|
|
1857
|
+
fetchDynamicModels: () =>
|
|
1858
|
+
fetchOpenAICompatibleModels<Api>({
|
|
1859
|
+
api: "openai-completions",
|
|
1860
|
+
provider: providerId,
|
|
1861
|
+
baseUrl: discoveryBaseUrl,
|
|
1862
|
+
apiKey,
|
|
1863
|
+
mapModel: (entry, defaults) => {
|
|
1864
|
+
const reference = references.get(defaults.id);
|
|
1865
|
+
const name = toModelName(entry.name, reference?.name ?? defaults.name);
|
|
1866
|
+
if (!reference) {
|
|
1867
|
+
return {
|
|
1868
|
+
...defaults,
|
|
1869
|
+
name,
|
|
1870
|
+
};
|
|
1871
|
+
}
|
|
1872
|
+
return {
|
|
1873
|
+
...reference,
|
|
1874
|
+
id: defaults.id,
|
|
1875
|
+
name,
|
|
1876
|
+
baseUrl: openCodeBaseUrlForApi(reference.api, basePath),
|
|
1877
|
+
contextWindow: toPositiveNumber(entry.context_length, reference.contextWindow),
|
|
1878
|
+
maxTokens: toPositiveNumber(entry.max_completion_tokens, reference.maxTokens),
|
|
1879
|
+
};
|
|
1880
|
+
},
|
|
1881
|
+
fetch: config?.fetch,
|
|
1882
|
+
}),
|
|
1883
|
+
}),
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
export function opencodeZenModelManagerOptions(config?: OpenCodeModelManagerConfig): ModelManagerOptions<Api> {
|
|
1888
|
+
return openCodeModelManagerOptions("opencode-zen", "https://opencode.ai/zen", config);
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
export function opencodeGoModelManagerOptions(config?: OpenCodeModelManagerConfig): ModelManagerOptions<Api> {
|
|
1892
|
+
return openCodeModelManagerOptions("opencode-go", "https://opencode.ai/zen/go", config);
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
// ---------------------------------------------------------------------------
|
|
1896
|
+
// 9. Ollama
|
|
1897
|
+
// ---------------------------------------------------------------------------
|
|
1898
|
+
|
|
1899
|
+
export interface OllamaModelManagerConfig {
|
|
1900
|
+
apiKey?: string;
|
|
1901
|
+
baseUrl?: string;
|
|
1902
|
+
fetch?: FetchImpl;
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
export function ollamaModelManagerOptions(config?: OllamaModelManagerConfig): ModelManagerOptions<"openai-responses"> {
|
|
1906
|
+
const apiKey = config?.apiKey;
|
|
1907
|
+
const baseUrl = normalizeOllamaBaseUrl(config?.baseUrl);
|
|
1908
|
+
const nativeBaseUrl = toOllamaNativeBaseUrl(baseUrl);
|
|
1909
|
+
const references = createBundledReferenceMap<"openai-responses">("ollama" as Parameters<typeof getBundledModels>[0]);
|
|
1910
|
+
const resolveMetadata = createOllamaMetadataResolver(nativeBaseUrl, config?.fetch);
|
|
1911
|
+
return {
|
|
1912
|
+
providerId: "ollama",
|
|
1913
|
+
fetchDynamicModels: async () => {
|
|
1914
|
+
const openAiCompatible = await fetchOpenAICompatibleModels({
|
|
1915
|
+
api: "openai-responses",
|
|
1916
|
+
provider: "ollama",
|
|
1917
|
+
baseUrl,
|
|
1918
|
+
apiKey,
|
|
1919
|
+
mapModel: (entry, defaults) => {
|
|
1920
|
+
const reference = references.get(defaults.id);
|
|
1921
|
+
if (!reference) {
|
|
1922
|
+
return {
|
|
1923
|
+
...defaults,
|
|
1924
|
+
name: toModelName(entry.name, defaults.name),
|
|
1925
|
+
contextWindow: OLLAMA_FALLBACK_CONTEXT_WINDOW,
|
|
1926
|
+
maxTokens: OLLAMA_DEFAULT_MAX_TOKENS,
|
|
1927
|
+
};
|
|
1928
|
+
}
|
|
1929
|
+
return mapWithBundledReference(entry, defaults, reference);
|
|
1930
|
+
},
|
|
1931
|
+
fetch: config?.fetch,
|
|
1932
|
+
});
|
|
1933
|
+
if (openAiCompatible && openAiCompatible.length > 0) {
|
|
1934
|
+
await Promise.all(
|
|
1935
|
+
openAiCompatible.map(async model => {
|
|
1936
|
+
const metadata = await resolveMetadata(model.id);
|
|
1937
|
+
model.contextWindow = metadata.contextWindow;
|
|
1938
|
+
if (metadata.reasoning !== undefined) {
|
|
1939
|
+
model.reasoning = metadata.reasoning;
|
|
1940
|
+
model.thinking = metadata.thinking;
|
|
1941
|
+
}
|
|
1942
|
+
if (metadata.input) {
|
|
1943
|
+
model.input = metadata.input;
|
|
1944
|
+
}
|
|
1945
|
+
}),
|
|
1946
|
+
);
|
|
1947
|
+
return openAiCompatible;
|
|
1948
|
+
}
|
|
1949
|
+
const nativeFallback = await fetchOllamaNativeModels(baseUrl, resolveMetadata, config?.fetch);
|
|
1950
|
+
if (nativeFallback && nativeFallback.length > 0) {
|
|
1951
|
+
return nativeFallback;
|
|
1952
|
+
}
|
|
1953
|
+
return openAiCompatible;
|
|
1954
|
+
},
|
|
1955
|
+
};
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
// ---------------------------------------------------------------------------
|
|
1959
|
+
// 10. OpenRouter
|
|
1960
|
+
// ---------------------------------------------------------------------------
|
|
1961
|
+
|
|
1962
|
+
export interface OpenRouterModelManagerConfig {
|
|
1963
|
+
apiKey?: string;
|
|
1964
|
+
baseUrl?: string;
|
|
1965
|
+
fetch?: FetchImpl;
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
export function openrouterModelManagerOptions(
|
|
1969
|
+
config?: OpenRouterModelManagerConfig,
|
|
1970
|
+
): ModelManagerOptions<"openrouter"> {
|
|
1971
|
+
const apiKey = config?.apiKey;
|
|
1972
|
+
const baseUrl = config?.baseUrl ?? "https://openrouter.ai/api/v1";
|
|
1973
|
+
const references = createBundledReferenceMap<"openrouter">("openrouter");
|
|
1974
|
+
return {
|
|
1975
|
+
providerId: "openrouter",
|
|
1976
|
+
// Older builds cached OpenRouter discovery rows as `api: "openai-completions"`.
|
|
1977
|
+
// Namespace the refreshed pseudo-API cache separately so those rows cannot
|
|
1978
|
+
// override bundled `api: "openrouter"` models during online-if-uncached startup.
|
|
1979
|
+
cacheProviderId: "openrouter:pseudo-api",
|
|
1980
|
+
fetchDynamicModels: () =>
|
|
1981
|
+
fetchOpenAICompatibleModels({
|
|
1982
|
+
api: "openrouter",
|
|
1983
|
+
provider: "openrouter",
|
|
1984
|
+
baseUrl,
|
|
1985
|
+
apiKey,
|
|
1986
|
+
filterModel: (entry: OpenAICompatibleModelRecord) => {
|
|
1987
|
+
const params = entry.supported_parameters;
|
|
1988
|
+
return Array.isArray(params) && params.includes("tools");
|
|
1989
|
+
},
|
|
1990
|
+
mapModel: (
|
|
1991
|
+
entry: OpenAICompatibleModelRecord,
|
|
1992
|
+
defaults: ModelSpec<"openrouter">,
|
|
1993
|
+
_context: OpenAICompatibleModelMapperContext<"openrouter">,
|
|
1994
|
+
): ModelSpec<"openrouter"> => {
|
|
1995
|
+
const reference = references.get(defaults.id);
|
|
1996
|
+
const baseModel = mapWithBundledReference(entry, defaults, reference);
|
|
1997
|
+
const pricing = entry.pricing as Record<string, unknown> | undefined;
|
|
1998
|
+
const params = Array.isArray(entry.supported_parameters) ? (entry.supported_parameters as string[]) : [];
|
|
1999
|
+
const modality = String((entry.architecture as Record<string, unknown> | undefined)?.modality ?? "");
|
|
2000
|
+
const topProvider = entry.top_provider as Record<string, unknown> | undefined;
|
|
2001
|
+
|
|
2002
|
+
const supportsToolChoice = params.includes("tool_choice");
|
|
2003
|
+
|
|
2004
|
+
return {
|
|
2005
|
+
...baseModel,
|
|
2006
|
+
reasoning: params.includes("reasoning"),
|
|
2007
|
+
input: modality.includes("image") ? ["text", "image"] : ["text"],
|
|
2008
|
+
cost: {
|
|
2009
|
+
input: parseFloat(String(pricing?.prompt ?? "0")) * 1_000_000,
|
|
2010
|
+
output: parseFloat(String(pricing?.completion ?? "0")) * 1_000_000,
|
|
2011
|
+
cacheRead: parseFloat(String(pricing?.input_cache_read ?? "0")) * 1_000_000,
|
|
2012
|
+
cacheWrite: parseFloat(String(pricing?.input_cache_write ?? "0")) * 1_000_000,
|
|
2013
|
+
},
|
|
2014
|
+
contextWindow:
|
|
2015
|
+
typeof entry.context_length === "number" ? entry.context_length : baseModel.contextWindow,
|
|
2016
|
+
maxTokens:
|
|
2017
|
+
typeof topProvider?.max_completion_tokens === "number"
|
|
2018
|
+
? topProvider.max_completion_tokens
|
|
2019
|
+
: baseModel.maxTokens,
|
|
2020
|
+
...(!supportsToolChoice && {
|
|
2021
|
+
compat: { ...(baseModel.compat ?? {}), supportsToolChoice: false },
|
|
2022
|
+
}),
|
|
2023
|
+
};
|
|
2024
|
+
},
|
|
2025
|
+
fetch: config?.fetch,
|
|
2026
|
+
}),
|
|
2027
|
+
};
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
const ZENMUX_OPENAI_BASE_URL = "https://zenmux.ai/api/v1";
|
|
2031
|
+
const ZENMUX_ANTHROPIC_BASE_URL = "https://zenmux.ai/api/anthropic";
|
|
2032
|
+
|
|
2033
|
+
function normalizeZenMuxOpenAiBaseUrl(baseUrl?: string): string {
|
|
2034
|
+
const value = baseUrl?.trim();
|
|
2035
|
+
if (!value) {
|
|
2036
|
+
return ZENMUX_OPENAI_BASE_URL;
|
|
2037
|
+
}
|
|
2038
|
+
return value.endsWith("/") ? value.slice(0, -1) : value;
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
function toZenMuxAnthropicBaseUrl(openAiBaseUrl: string): string {
|
|
2042
|
+
try {
|
|
2043
|
+
const parsed = new URL(openAiBaseUrl);
|
|
2044
|
+
const trimmedPath = parsed.pathname.replace(/\/+$/g, "");
|
|
2045
|
+
parsed.pathname = trimmedPath.endsWith("/api/v1")
|
|
2046
|
+
? `${trimmedPath.slice(0, -"/api/v1".length)}/api/anthropic`
|
|
2047
|
+
: "/api/anthropic";
|
|
2048
|
+
return `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
|
|
2049
|
+
} catch {
|
|
2050
|
+
return ZENMUX_ANTHROPIC_BASE_URL;
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
function isZenMuxAnthropicModel(entry: OpenAICompatibleModelRecord, modelId: string): boolean {
|
|
2055
|
+
if (typeof entry.owned_by === "string" && entry.owned_by.toLowerCase() === "anthropic") {
|
|
2056
|
+
return true;
|
|
2057
|
+
}
|
|
2058
|
+
return modelId.toLowerCase().startsWith("anthropic/");
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
function getZenMuxPricingValue(pricings: Record<string, unknown> | undefined, key: string): number {
|
|
2062
|
+
const bucket = pricings?.[key];
|
|
2063
|
+
if (!Array.isArray(bucket)) {
|
|
2064
|
+
return 0;
|
|
2065
|
+
}
|
|
2066
|
+
for (const item of bucket) {
|
|
2067
|
+
if (!isRecord(item)) {
|
|
2068
|
+
continue;
|
|
2069
|
+
}
|
|
2070
|
+
const value = toNumber(item.value);
|
|
2071
|
+
if (value !== undefined) {
|
|
2072
|
+
return value;
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
return 0;
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
function getZenMuxCacheWritePrice(pricings: Record<string, unknown> | undefined): number {
|
|
2079
|
+
const oneHour = getZenMuxPricingValue(pricings, "input_cache_write_1_h");
|
|
2080
|
+
if (oneHour > 0) {
|
|
2081
|
+
return oneHour;
|
|
2082
|
+
}
|
|
2083
|
+
const fiveMinute = getZenMuxPricingValue(pricings, "input_cache_write_5_min");
|
|
2084
|
+
if (fiveMinute > 0) {
|
|
2085
|
+
return fiveMinute;
|
|
2086
|
+
}
|
|
2087
|
+
return getZenMuxPricingValue(pricings, "input_cache_write");
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
// ---------------------------------------------------------------------------
|
|
2091
|
+
// 10.5 ZenMux
|
|
2092
|
+
// ---------------------------------------------------------------------------
|
|
2093
|
+
|
|
2094
|
+
export interface ZenMuxModelManagerConfig {
|
|
2095
|
+
apiKey?: string;
|
|
2096
|
+
baseUrl?: string;
|
|
2097
|
+
fetch?: FetchImpl;
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
export function zenmuxModelManagerOptions(config?: ZenMuxModelManagerConfig): ModelManagerOptions<Api> {
|
|
2101
|
+
const apiKey = config?.apiKey;
|
|
2102
|
+
const openAiBaseUrl = normalizeZenMuxOpenAiBaseUrl(config?.baseUrl);
|
|
2103
|
+
const anthropicBaseUrl = toZenMuxAnthropicBaseUrl(openAiBaseUrl);
|
|
2104
|
+
return {
|
|
2105
|
+
providerId: "zenmux",
|
|
2106
|
+
...(apiKey && {
|
|
2107
|
+
fetchDynamicModels: () =>
|
|
2108
|
+
fetchOpenAICompatibleModels<Api>({
|
|
2109
|
+
api: "openai-completions",
|
|
2110
|
+
provider: "zenmux",
|
|
2111
|
+
baseUrl: openAiBaseUrl,
|
|
2112
|
+
apiKey,
|
|
2113
|
+
mapModel: (entry, defaults) => {
|
|
2114
|
+
const pricings = isRecord(entry.pricings) ? entry.pricings : undefined;
|
|
2115
|
+
const capabilities = isRecord(entry.capabilities) ? entry.capabilities : undefined;
|
|
2116
|
+
const isAnthropicModel = isZenMuxAnthropicModel(entry, defaults.id);
|
|
2117
|
+
return {
|
|
2118
|
+
...defaults,
|
|
2119
|
+
name: toModelName(entry.display_name, defaults.name),
|
|
2120
|
+
api: isAnthropicModel ? "anthropic-messages" : "openai-completions",
|
|
2121
|
+
baseUrl: isAnthropicModel ? anthropicBaseUrl : openAiBaseUrl,
|
|
2122
|
+
reasoning: capabilities?.reasoning === true || defaults.reasoning,
|
|
2123
|
+
input: toInputCapabilities(entry.input_modalities),
|
|
2124
|
+
cost: {
|
|
2125
|
+
input: getZenMuxPricingValue(pricings, "prompt"),
|
|
2126
|
+
output: getZenMuxPricingValue(pricings, "completion"),
|
|
2127
|
+
cacheRead: getZenMuxPricingValue(pricings, "input_cache_read"),
|
|
2128
|
+
cacheWrite: getZenMuxCacheWritePrice(pricings),
|
|
2129
|
+
},
|
|
2130
|
+
contextWindow: toPositiveNumber(entry.context_length, defaults.contextWindow),
|
|
2131
|
+
maxTokens: toPositiveNumber(entry.max_completion_tokens, defaults.maxTokens),
|
|
2132
|
+
};
|
|
2133
|
+
},
|
|
2134
|
+
fetch: config?.fetch,
|
|
2135
|
+
}),
|
|
2136
|
+
}),
|
|
2137
|
+
};
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
// ---------------------------------------------------------------------------
|
|
2141
|
+
// 10.6 Kilo Gateway
|
|
2142
|
+
// ---------------------------------------------------------------------------
|
|
2143
|
+
|
|
2144
|
+
export interface KiloModelManagerConfig {
|
|
2145
|
+
apiKey?: string;
|
|
2146
|
+
baseUrl?: string;
|
|
2147
|
+
fetch?: FetchImpl;
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
export function kiloModelManagerOptions(config?: KiloModelManagerConfig): ModelManagerOptions<"openai-completions"> {
|
|
2151
|
+
const apiKey = config?.apiKey;
|
|
2152
|
+
const baseUrl = config?.baseUrl ?? "https://api.kilo.ai/api/gateway";
|
|
2153
|
+
return {
|
|
2154
|
+
providerId: "kilo",
|
|
2155
|
+
fetchDynamicModels: () =>
|
|
2156
|
+
fetchOpenAICompatibleModels({
|
|
2157
|
+
api: "openai-completions",
|
|
2158
|
+
provider: "kilo",
|
|
2159
|
+
baseUrl,
|
|
2160
|
+
apiKey,
|
|
2161
|
+
fetch: config?.fetch,
|
|
2162
|
+
}),
|
|
2163
|
+
};
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
// ---------------------------------------------------------------------------
|
|
2167
|
+
// Alibaba Coding Plan
|
|
2168
|
+
// ---------------------------------------------------------------------------
|
|
2169
|
+
|
|
2170
|
+
export interface AlibabaCodingPlanModelManagerConfig {
|
|
2171
|
+
apiKey?: string;
|
|
2172
|
+
baseUrl?: string;
|
|
2173
|
+
fetch?: FetchImpl;
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
export function alibabaCodingPlanModelManagerOptions(
|
|
2177
|
+
config?: AlibabaCodingPlanModelManagerConfig,
|
|
2178
|
+
): ModelManagerOptions<"openai-completions"> {
|
|
2179
|
+
const apiKey = config?.apiKey;
|
|
2180
|
+
const baseUrl = config?.baseUrl ?? "https://coding-intl.dashscope.aliyuncs.com/v1";
|
|
2181
|
+
const references = createBundledReferenceMap<"openai-completions">("alibaba-coding-plan");
|
|
2182
|
+
return {
|
|
2183
|
+
providerId: "alibaba-coding-plan",
|
|
2184
|
+
fetchDynamicModels: () =>
|
|
2185
|
+
fetchOpenAICompatibleModels({
|
|
2186
|
+
api: "openai-completions",
|
|
2187
|
+
provider: "alibaba-coding-plan",
|
|
2188
|
+
baseUrl,
|
|
2189
|
+
apiKey,
|
|
2190
|
+
mapModel: (entry, defaults) => {
|
|
2191
|
+
const reference = references.get(defaults.id);
|
|
2192
|
+
return mapWithBundledReference(entry, defaults, reference);
|
|
2193
|
+
},
|
|
2194
|
+
fetch: config?.fetch,
|
|
2195
|
+
}),
|
|
2196
|
+
};
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
// ---------------------------------------------------------------------------
|
|
2200
|
+
// 11. Vercel AI Gateway
|
|
2201
|
+
// ---------------------------------------------------------------------------
|
|
2202
|
+
|
|
2203
|
+
export interface VercelAiGatewayModelManagerConfig {
|
|
2204
|
+
apiKey?: string;
|
|
2205
|
+
baseUrl?: string;
|
|
2206
|
+
fetch?: FetchImpl;
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
function normalizeVercelAiGatewayBaseUrls(rawBaseUrl: string | undefined): { baseUrl: string; catalogBaseUrl: string } {
|
|
2210
|
+
const baseUrl = (rawBaseUrl === undefined ? "https://ai-gateway.vercel.sh" : rawBaseUrl.trim()).replace(/\/+$/, "");
|
|
2211
|
+
const catalogBaseUrl = baseUrl === "" || baseUrl.endsWith("/v1") ? baseUrl : `${baseUrl}/v1`;
|
|
2212
|
+
|
|
2213
|
+
return {
|
|
2214
|
+
baseUrl: baseUrl.endsWith("/v1") ? baseUrl.slice(0, -3) : baseUrl,
|
|
2215
|
+
catalogBaseUrl,
|
|
2216
|
+
};
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
export function vercelAiGatewayModelManagerOptions(
|
|
2220
|
+
config?: VercelAiGatewayModelManagerConfig,
|
|
2221
|
+
): ModelManagerOptions<"anthropic-messages"> {
|
|
2222
|
+
const apiKey = config?.apiKey;
|
|
2223
|
+
const { baseUrl, catalogBaseUrl } = normalizeVercelAiGatewayBaseUrls(config?.baseUrl);
|
|
2224
|
+
return {
|
|
2225
|
+
providerId: "vercel-ai-gateway",
|
|
2226
|
+
fetchDynamicModels: () =>
|
|
2227
|
+
fetchOpenAICompatibleModels({
|
|
2228
|
+
api: "anthropic-messages",
|
|
2229
|
+
provider: "vercel-ai-gateway",
|
|
2230
|
+
baseUrl: catalogBaseUrl,
|
|
2231
|
+
apiKey,
|
|
2232
|
+
filterModel: (entry: OpenAICompatibleModelRecord) => {
|
|
2233
|
+
const tags = entry.tags;
|
|
2234
|
+
return Array.isArray(tags) && tags.includes("tool-use");
|
|
2235
|
+
},
|
|
2236
|
+
mapModel: (
|
|
2237
|
+
entry: OpenAICompatibleModelRecord,
|
|
2238
|
+
defaults: ModelSpec<"anthropic-messages">,
|
|
2239
|
+
_context: OpenAICompatibleModelMapperContext<"anthropic-messages">,
|
|
2240
|
+
): ModelSpec<"anthropic-messages"> => {
|
|
2241
|
+
const pricing = entry.pricing as Record<string, unknown> | undefined;
|
|
2242
|
+
const tags = Array.isArray(entry.tags) ? (entry.tags as string[]) : [];
|
|
2243
|
+
|
|
2244
|
+
return {
|
|
2245
|
+
...defaults,
|
|
2246
|
+
baseUrl,
|
|
2247
|
+
reasoning: tags.includes("reasoning"),
|
|
2248
|
+
input: tags.includes("vision") ? ["text", "image"] : ["text"],
|
|
2249
|
+
cost: {
|
|
2250
|
+
input: (toNumber(pricing?.input) ?? 0) * 1_000_000,
|
|
2251
|
+
output: (toNumber(pricing?.output) ?? 0) * 1_000_000,
|
|
2252
|
+
cacheRead: (toNumber(pricing?.input_cache_read) ?? 0) * 1_000_000,
|
|
2253
|
+
cacheWrite: (toNumber(pricing?.input_cache_write) ?? 0) * 1_000_000,
|
|
2254
|
+
},
|
|
2255
|
+
contextWindow:
|
|
2256
|
+
typeof entry.context_window === "number" ? entry.context_window : defaults.contextWindow,
|
|
2257
|
+
maxTokens: typeof entry.max_tokens === "number" ? entry.max_tokens : defaults.maxTokens,
|
|
2258
|
+
};
|
|
2259
|
+
},
|
|
2260
|
+
fetch: config?.fetch,
|
|
2261
|
+
}),
|
|
2262
|
+
};
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
// ---------------------------------------------------------------------------
|
|
2266
|
+
// 12. Kimi Code
|
|
2267
|
+
// ---------------------------------------------------------------------------
|
|
2268
|
+
|
|
2269
|
+
export interface KimiCodeModelManagerConfig {
|
|
2270
|
+
apiKey?: string;
|
|
2271
|
+
baseUrl?: string;
|
|
2272
|
+
fetch?: FetchImpl;
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
export function kimiCodeModelManagerOptions(
|
|
2276
|
+
config?: KimiCodeModelManagerConfig,
|
|
2277
|
+
): ModelManagerOptions<"openai-completions"> {
|
|
2278
|
+
const apiKey = config?.apiKey;
|
|
2279
|
+
const baseUrl = config?.baseUrl ?? "https://api.kimi.com/coding/v1";
|
|
2280
|
+
return {
|
|
2281
|
+
providerId: "kimi-code",
|
|
2282
|
+
...(apiKey && {
|
|
2283
|
+
fetchDynamicModels: () =>
|
|
2284
|
+
fetchOpenAICompatibleModels({
|
|
2285
|
+
api: "openai-completions",
|
|
2286
|
+
provider: "kimi-code",
|
|
2287
|
+
baseUrl,
|
|
2288
|
+
apiKey,
|
|
2289
|
+
headers: {
|
|
2290
|
+
"User-Agent": "KimiCLI/1.0",
|
|
2291
|
+
"X-Msh-Platform": "kimi_cli",
|
|
2292
|
+
},
|
|
2293
|
+
mapModel: (
|
|
2294
|
+
entry: OpenAICompatibleModelRecord,
|
|
2295
|
+
defaults: ModelSpec<"openai-completions">,
|
|
2296
|
+
_context: OpenAICompatibleModelMapperContext<"openai-completions">,
|
|
2297
|
+
): ModelSpec<"openai-completions"> => {
|
|
2298
|
+
const id = defaults.id;
|
|
2299
|
+
return {
|
|
2300
|
+
...defaults,
|
|
2301
|
+
name: typeof entry.display_name === "string" ? entry.display_name : defaults.name,
|
|
2302
|
+
reasoning: entry.supports_reasoning === true || id.includes("thinking"),
|
|
2303
|
+
input: entry.supports_image_in === true || id.includes("k2.5") ? ["text", "image"] : ["text"],
|
|
2304
|
+
contextWindow: typeof entry.context_length === "number" ? entry.context_length : 262144,
|
|
2305
|
+
maxTokens: 32000,
|
|
2306
|
+
compat: {
|
|
2307
|
+
thinkingFormat: "zai",
|
|
2308
|
+
reasoningContentField: "reasoning_content",
|
|
2309
|
+
supportsDeveloperRole: false,
|
|
2310
|
+
},
|
|
2311
|
+
};
|
|
2312
|
+
},
|
|
2313
|
+
fetch: config?.fetch,
|
|
2314
|
+
}),
|
|
2315
|
+
}),
|
|
2316
|
+
};
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
// ---------------------------------------------------------------------------
|
|
2320
|
+
// 12.5. LM Studio
|
|
2321
|
+
// ---------------------------------------------------------------------------
|
|
2322
|
+
|
|
2323
|
+
/** Native LM Studio metadata keyed by model id from `/api/v0/models`. */
|
|
2324
|
+
export interface LmStudioNativeModelMetadata {
|
|
2325
|
+
input: ("text" | "image")[];
|
|
2326
|
+
contextWindow?: number;
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
/** Options for LM Studio's optional native metadata probe. */
|
|
2330
|
+
export interface LmStudioNativeModelMetadataOptions {
|
|
2331
|
+
headers?: Record<string, string>;
|
|
2332
|
+
signal?: AbortSignal;
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
const LM_STUDIO_NATIVE_METADATA_TIMEOUT_MS = 250;
|
|
2336
|
+
|
|
2337
|
+
function toLmStudioNativeBaseUrl(baseUrl: string): string {
|
|
2338
|
+
const trimmed = baseUrl.trim();
|
|
2339
|
+
const normalized = trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed;
|
|
2340
|
+
return normalized.endsWith("/v1") ? normalized.slice(0, -3) : normalized;
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
function getLmStudioCapabilityNames(value: unknown): string[] {
|
|
2344
|
+
if (!Array.isArray(value)) {
|
|
2345
|
+
return [];
|
|
2346
|
+
}
|
|
2347
|
+
return value.flatMap(item => (typeof item === "string" ? [item.toLowerCase()] : []));
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
function getLmStudioNativeInput(entry: Record<string, unknown>): ("text" | "image")[] {
|
|
2351
|
+
const modelType = typeof entry.type === "string" ? entry.type.toLowerCase() : "";
|
|
2352
|
+
const capabilities = getLmStudioCapabilityNames(entry.capabilities);
|
|
2353
|
+
const supportsImage = modelType === "vlm" || capabilities.includes("vision") || capabilities.includes("image");
|
|
2354
|
+
return supportsImage ? ["text", "image"] : ["text"];
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
function getLmStudioNativeContextWindow(entry: Record<string, unknown>): number | undefined {
|
|
2358
|
+
return (
|
|
2359
|
+
toPositiveNumber(entry.max_context_length, null) ??
|
|
2360
|
+
toPositiveNumber(entry.context_length, null) ??
|
|
2361
|
+
toPositiveNumber(entry.max_model_len, null) ??
|
|
2362
|
+
undefined
|
|
2363
|
+
);
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
/** Fetches LM Studio native model metadata used to mark VLM models as image-capable. */
|
|
2367
|
+
export async function fetchLmStudioNativeModelMetadata(
|
|
2368
|
+
baseUrl: string,
|
|
2369
|
+
fetchImpl: FetchImpl = fetch,
|
|
2370
|
+
options?: LmStudioNativeModelMetadataOptions,
|
|
2371
|
+
): Promise<Map<string, LmStudioNativeModelMetadata> | null> {
|
|
2372
|
+
const nativeBaseUrl = toLmStudioNativeBaseUrl(baseUrl);
|
|
2373
|
+
const fetchMetadata = async (signal?: AbortSignal): Promise<Map<string, LmStudioNativeModelMetadata> | null> => {
|
|
2374
|
+
try {
|
|
2375
|
+
const response = await fetchImpl(`${nativeBaseUrl}/api/v0/models`, {
|
|
2376
|
+
method: "GET",
|
|
2377
|
+
headers: { Accept: "application/json", ...(options?.headers ?? {}) },
|
|
2378
|
+
signal,
|
|
2379
|
+
});
|
|
2380
|
+
if (!response.ok) {
|
|
2381
|
+
return null;
|
|
2382
|
+
}
|
|
2383
|
+
const payload = await response.json();
|
|
2384
|
+
if (!isRecord(payload) || !Array.isArray(payload.data)) {
|
|
2385
|
+
return null;
|
|
2386
|
+
}
|
|
2387
|
+
const metadata = new Map<string, LmStudioNativeModelMetadata>();
|
|
2388
|
+
for (const entry of payload.data) {
|
|
2389
|
+
if (!isRecord(entry) || typeof entry.id !== "string" || entry.id.length === 0) {
|
|
2390
|
+
continue;
|
|
2391
|
+
}
|
|
2392
|
+
const contextWindow = getLmStudioNativeContextWindow(entry);
|
|
2393
|
+
metadata.set(entry.id, {
|
|
2394
|
+
input: getLmStudioNativeInput(entry),
|
|
2395
|
+
...(contextWindow === undefined ? {} : { contextWindow }),
|
|
2396
|
+
});
|
|
2397
|
+
}
|
|
2398
|
+
return metadata;
|
|
2399
|
+
} catch {
|
|
2400
|
+
return null;
|
|
2401
|
+
}
|
|
2402
|
+
};
|
|
2403
|
+
if (options?.signal !== undefined) {
|
|
2404
|
+
return fetchMetadata(options.signal);
|
|
2405
|
+
}
|
|
2406
|
+
return withCatalogDiscoveryTimeout(LM_STUDIO_NATIVE_METADATA_TIMEOUT_MS, fetchMetadata);
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
export interface LmStudioModelManagerConfig {
|
|
2410
|
+
apiKey?: string;
|
|
2411
|
+
baseUrl?: string;
|
|
2412
|
+
fetch?: FetchImpl;
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
export function lmStudioModelManagerOptions(
|
|
2416
|
+
config?: LmStudioModelManagerConfig,
|
|
2417
|
+
): ModelManagerOptions<"openai-completions"> {
|
|
2418
|
+
const apiKey = config?.apiKey;
|
|
2419
|
+
const baseUrl = config?.baseUrl ?? Bun.env.LM_STUDIO_BASE_URL ?? "http://127.0.0.1:1234/v1";
|
|
2420
|
+
const references = createBundledReferenceMap<"openai-completions">("lm-studio" as any);
|
|
2421
|
+
return {
|
|
2422
|
+
providerId: "lm-studio",
|
|
2423
|
+
fetchDynamicModels: async () => {
|
|
2424
|
+
const nativeMetadataPromise = fetchLmStudioNativeModelMetadata(baseUrl, config?.fetch, {
|
|
2425
|
+
headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : undefined,
|
|
2426
|
+
});
|
|
2427
|
+
const models = await fetchOpenAICompatibleModels({
|
|
2428
|
+
api: "openai-completions",
|
|
2429
|
+
provider: "lm-studio",
|
|
2430
|
+
baseUrl,
|
|
2431
|
+
apiKey,
|
|
2432
|
+
mapModel: (entry, defaults) => {
|
|
2433
|
+
const reference = references.get(defaults.id);
|
|
2434
|
+
return mapWithBundledReference(entry, defaults, reference);
|
|
2435
|
+
},
|
|
2436
|
+
fetch: config?.fetch,
|
|
2437
|
+
});
|
|
2438
|
+
if (!models) {
|
|
2439
|
+
return models;
|
|
2440
|
+
}
|
|
2441
|
+
const nativeMetadata = await nativeMetadataPromise;
|
|
2442
|
+
if (!nativeMetadata) {
|
|
2443
|
+
return models;
|
|
2444
|
+
}
|
|
2445
|
+
return models.map(model => {
|
|
2446
|
+
const metadata = nativeMetadata.get(model.id);
|
|
2447
|
+
if (!metadata) {
|
|
2448
|
+
return model;
|
|
2449
|
+
}
|
|
2450
|
+
return {
|
|
2451
|
+
...model,
|
|
2452
|
+
input: metadata.input,
|
|
2453
|
+
contextWindow: metadata.contextWindow ?? model.contextWindow,
|
|
2454
|
+
};
|
|
2455
|
+
});
|
|
2456
|
+
},
|
|
2457
|
+
};
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
// ---------------------------------------------------------------------------
|
|
2461
|
+
// 13. Synthetic
|
|
2462
|
+
// ---------------------------------------------------------------------------
|
|
2463
|
+
|
|
2464
|
+
export interface SyntheticModelManagerConfig {
|
|
2465
|
+
apiKey?: string;
|
|
2466
|
+
baseUrl?: string;
|
|
2467
|
+
fetch?: FetchImpl;
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2470
|
+
export function syntheticModelManagerOptions(
|
|
2471
|
+
config?: SyntheticModelManagerConfig,
|
|
2472
|
+
): ModelManagerOptions<"openai-completions"> {
|
|
2473
|
+
const apiKey = config?.apiKey;
|
|
2474
|
+
const baseUrl = config?.baseUrl ?? "https://api.synthetic.new/openai/v1";
|
|
2475
|
+
const references = new Map(
|
|
2476
|
+
(getBundledModels("synthetic") as Model<"openai-completions">[]).map(model => [model.id, toModelSpec(model)]),
|
|
2477
|
+
);
|
|
2478
|
+
return {
|
|
2479
|
+
providerId: "synthetic",
|
|
2480
|
+
dynamicModelsAuthoritative: true,
|
|
2481
|
+
...(apiKey && {
|
|
2482
|
+
fetchDynamicModels: () =>
|
|
2483
|
+
fetchOpenAICompatibleModels({
|
|
2484
|
+
api: "openai-completions",
|
|
2485
|
+
provider: "synthetic",
|
|
2486
|
+
baseUrl,
|
|
2487
|
+
apiKey,
|
|
2488
|
+
mapModel: (
|
|
2489
|
+
entry: OpenAICompatibleModelRecord,
|
|
2490
|
+
defaults: ModelSpec<"openai-completions">,
|
|
2491
|
+
_context: OpenAICompatibleModelMapperContext<"openai-completions">,
|
|
2492
|
+
): ModelSpec<"openai-completions"> => {
|
|
2493
|
+
const reference = references.get(defaults.id);
|
|
2494
|
+
const referenceSupportsImage = reference?.input.includes("image") ?? false;
|
|
2495
|
+
return {
|
|
2496
|
+
...(reference ? { ...reference, id: defaults.id, baseUrl } : defaults),
|
|
2497
|
+
name: toModelName(entry.name, reference?.name ?? defaults.name),
|
|
2498
|
+
reasoning: entry.supports_reasoning === true || (reference?.reasoning ?? false),
|
|
2499
|
+
input: entry.supports_vision === true || referenceSupportsImage ? ["text", "image"] : ["text"],
|
|
2500
|
+
contextWindow: toPositiveNumber(
|
|
2501
|
+
entry.context_length,
|
|
2502
|
+
reference?.contextWindow ?? defaults.contextWindow,
|
|
2503
|
+
),
|
|
2504
|
+
maxTokens: toPositiveNumber(entry.max_tokens, reference?.maxTokens ?? 8192),
|
|
2505
|
+
};
|
|
2506
|
+
},
|
|
2507
|
+
fetch: config?.fetch,
|
|
2508
|
+
}),
|
|
2509
|
+
}),
|
|
2510
|
+
};
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
// ---------------------------------------------------------------------------
|
|
2514
|
+
// 14. Venice
|
|
2515
|
+
// ---------------------------------------------------------------------------
|
|
2516
|
+
|
|
2517
|
+
export interface VeniceModelManagerConfig {
|
|
2518
|
+
apiKey?: string;
|
|
2519
|
+
baseUrl?: string;
|
|
2520
|
+
fetch?: FetchImpl;
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
export function veniceModelManagerOptions(
|
|
2524
|
+
config?: VeniceModelManagerConfig,
|
|
2525
|
+
): ModelManagerOptions<"openai-completions"> {
|
|
2526
|
+
const apiKey = config?.apiKey;
|
|
2527
|
+
const baseUrl = config?.baseUrl ?? "https://api.venice.ai/api/v1";
|
|
2528
|
+
const references = createBundledReferenceMap<"openai-completions">("venice");
|
|
2529
|
+
return {
|
|
2530
|
+
providerId: "venice",
|
|
2531
|
+
fetchDynamicModels: () =>
|
|
2532
|
+
fetchOpenAICompatibleModels({
|
|
2533
|
+
api: "openai-completions",
|
|
2534
|
+
provider: "venice",
|
|
2535
|
+
baseUrl,
|
|
2536
|
+
apiKey,
|
|
2537
|
+
mapModel: (entry, defaults) => {
|
|
2538
|
+
const reference = references.get(defaults.id);
|
|
2539
|
+
const model = mapWithBundledReference(entry, defaults, reference);
|
|
2540
|
+
return {
|
|
2541
|
+
...model,
|
|
2542
|
+
maxTokens: clampKimiK27CodeMaxTokens(defaults.id, model.maxTokens),
|
|
2543
|
+
compat: { ...model.compat, supportsUsageInStreaming: false },
|
|
2544
|
+
};
|
|
2545
|
+
},
|
|
2546
|
+
fetch: config?.fetch,
|
|
2547
|
+
}),
|
|
2548
|
+
};
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
// ---------------------------------------------------------------------------
|
|
2552
|
+
// 15. Together
|
|
2553
|
+
// ---------------------------------------------------------------------------
|
|
2554
|
+
|
|
2555
|
+
export interface TogetherModelManagerConfig {
|
|
2556
|
+
apiKey?: string;
|
|
2557
|
+
baseUrl?: string;
|
|
2558
|
+
fetch?: FetchImpl;
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
export function togetherModelManagerOptions(
|
|
2562
|
+
config?: TogetherModelManagerConfig,
|
|
2563
|
+
): ModelManagerOptions<"openai-completions"> {
|
|
2564
|
+
return createSimpleOpenAICompletionsOptions("together", "https://api.together.xyz/v1", config);
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
// ---------------------------------------------------------------------------
|
|
2568
|
+
// 15.5 CoreWeave Serverless Inference
|
|
2569
|
+
// ---------------------------------------------------------------------------
|
|
2570
|
+
|
|
2571
|
+
export interface CoreWeaveModelManagerConfig {
|
|
2572
|
+
apiKey?: string;
|
|
2573
|
+
baseUrl?: string;
|
|
2574
|
+
fetch?: FetchImpl;
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
export function coreWeaveModelManagerOptions(
|
|
2578
|
+
config?: CoreWeaveModelManagerConfig,
|
|
2579
|
+
): ModelManagerOptions<"openai-completions"> {
|
|
2580
|
+
return createSimpleOpenAICompletionsOptions("coreweave", "https://api.inference.wandb.ai/v1", {
|
|
2581
|
+
...config,
|
|
2582
|
+
headers: () => coreWeaveProjectHeaders(Bun.env),
|
|
2583
|
+
});
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
// ---------------------------------------------------------------------------
|
|
2587
|
+
// 16. Moonshot
|
|
2588
|
+
// ---------------------------------------------------------------------------
|
|
2589
|
+
|
|
2590
|
+
export interface MoonshotModelManagerConfig {
|
|
2591
|
+
apiKey?: string;
|
|
2592
|
+
baseUrl?: string;
|
|
2593
|
+
fetch?: FetchImpl;
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
export function moonshotModelManagerOptions(
|
|
2597
|
+
config?: MoonshotModelManagerConfig,
|
|
2598
|
+
): ModelManagerOptions<"openai-completions"> {
|
|
2599
|
+
const apiKey = config?.apiKey;
|
|
2600
|
+
// `MOONSHOT_BASE_URL` redirects discovery (and the streaming request that
|
|
2601
|
+
// inherits this baseUrl) at the Kimi China platform `api.moonshot.cn`; an
|
|
2602
|
+
// explicit `config.baseUrl` still wins. Mirrors LITELLM_BASE_URL/LM_STUDIO_BASE_URL. (#2883)
|
|
2603
|
+
const baseUrl = config?.baseUrl ?? Bun.env.MOONSHOT_BASE_URL ?? "https://api.moonshot.ai/v1";
|
|
2604
|
+
const references = createBundledReferenceMap<"openai-completions">("moonshot");
|
|
2605
|
+
return {
|
|
2606
|
+
providerId: "moonshot",
|
|
2607
|
+
...(apiKey && {
|
|
2608
|
+
fetchDynamicModels: () =>
|
|
2609
|
+
fetchOpenAICompatibleModels({
|
|
2610
|
+
api: "openai-completions",
|
|
2611
|
+
provider: "moonshot",
|
|
2612
|
+
baseUrl,
|
|
2613
|
+
apiKey,
|
|
2614
|
+
mapModel: (entry, defaults) => {
|
|
2615
|
+
const reference = references.get(defaults.id);
|
|
2616
|
+
const model = mapWithBundledReference(entry, defaults, reference);
|
|
2617
|
+
const id = model.id.toLowerCase();
|
|
2618
|
+
// Moonshot's K2.x family (K2.5, K2.6, kimi-k2-thinking, …) is reasoning-capable
|
|
2619
|
+
// and vision-capable on the native API. Without these flags the openai-completions
|
|
2620
|
+
// path skips the z.ai-format `thinking` block, and Moonshot K2.6 stalls on first
|
|
2621
|
+
// turn because its endpoint expects an explicit `thinking: {type}` (#2113). Match
|
|
2622
|
+
// the bundled K2.5 metadata for every K2.x id we discover.
|
|
2623
|
+
const isKimiK2Reasoning = id.includes("thinking") || /(^|\/)kimi-k2(?:\.\d+)?(?:[-:]|$)/.test(id);
|
|
2624
|
+
const isVision =
|
|
2625
|
+
id.includes("vision") || id.includes("vl") || /(^|\/)kimi-k2(?:\.\d+)?(?:[-:]|$)/.test(id);
|
|
2626
|
+
return {
|
|
2627
|
+
...model,
|
|
2628
|
+
reasoning: isKimiK2Reasoning || model.reasoning,
|
|
2629
|
+
input: isVision ? ["text", "image"] : model.input,
|
|
2630
|
+
thinking:
|
|
2631
|
+
model.thinking ??
|
|
2632
|
+
(isKimiK2Reasoning
|
|
2633
|
+
? { mode: "effort", efforts: [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High] }
|
|
2634
|
+
: undefined),
|
|
2635
|
+
};
|
|
2636
|
+
},
|
|
2637
|
+
fetch: config?.fetch,
|
|
2638
|
+
}),
|
|
2639
|
+
}),
|
|
2640
|
+
};
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
// ---------------------------------------------------------------------------
|
|
2644
|
+
// 16.5 Sakana AI
|
|
2645
|
+
// ---------------------------------------------------------------------------
|
|
2646
|
+
|
|
2647
|
+
const SAKANA_DEFAULT_BASE_URL = "https://api.sakana.ai/v1";
|
|
2648
|
+
const SAKANA_FREE_ROUTER_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } as const;
|
|
2649
|
+
const SAKANA_FUGU_ULTRA_COST = { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 0 } as const;
|
|
2650
|
+
const SAKANA_FUGU_ULTRA_CONTEXT_WINDOW = 1_000_000;
|
|
2651
|
+
const SAKANA_FUGU_THINKING: ThinkingConfig = {
|
|
2652
|
+
mode: "effort",
|
|
2653
|
+
efforts: [Effort.High, Effort.XHigh],
|
|
2654
|
+
effortMap: { [Effort.XHigh]: "max" },
|
|
2655
|
+
};
|
|
2656
|
+
const SAKANA_RESPONSES_COMPAT: ModelSpec<"openai-responses">["compat"] = {
|
|
2657
|
+
includeEncryptedReasoning: false,
|
|
2658
|
+
streamIdleTimeoutMs: 0,
|
|
2659
|
+
};
|
|
2660
|
+
|
|
2661
|
+
function normalizeSakanaBaseUrl(baseUrl: string | undefined): string {
|
|
2662
|
+
const value = baseUrl?.trim() || SAKANA_DEFAULT_BASE_URL;
|
|
2663
|
+
const normalized = value.replace(/\/+$/, "");
|
|
2664
|
+
return normalized.endsWith("/v1") ? normalized : `${normalized}/v1`;
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
function isSakanaFuguModelId(modelId: string): boolean {
|
|
2668
|
+
return /^fugu(?:$|-)/i.test(modelId);
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
function createSakanaFuguStaticModel(
|
|
2672
|
+
id: string,
|
|
2673
|
+
name: string,
|
|
2674
|
+
cost: ModelSpec<"openai-responses">["cost"],
|
|
2675
|
+
contextWindow: number | null,
|
|
2676
|
+
): ModelSpec<"openai-responses"> {
|
|
2677
|
+
return {
|
|
2678
|
+
id,
|
|
2679
|
+
name,
|
|
2680
|
+
api: "openai-responses",
|
|
2681
|
+
provider: "sakana",
|
|
2682
|
+
baseUrl: SAKANA_DEFAULT_BASE_URL,
|
|
2683
|
+
reasoning: true,
|
|
2684
|
+
input: ["text"],
|
|
2685
|
+
cost: { ...cost },
|
|
2686
|
+
contextWindow,
|
|
2687
|
+
maxTokens: null,
|
|
2688
|
+
thinking: { ...SAKANA_FUGU_THINKING },
|
|
2689
|
+
compat: { ...SAKANA_RESPONSES_COMPAT },
|
|
2690
|
+
};
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2693
|
+
export const SAKANA_FUGU_STATIC_MODELS: readonly ModelSpec<"openai-responses">[] = [
|
|
2694
|
+
createSakanaFuguStaticModel("fugu", "Fugu", SAKANA_FREE_ROUTER_COST, SAKANA_FUGU_ULTRA_CONTEXT_WINDOW),
|
|
2695
|
+
createSakanaFuguStaticModel("fugu-ultra", "Fugu Ultra", SAKANA_FUGU_ULTRA_COST, SAKANA_FUGU_ULTRA_CONTEXT_WINDOW),
|
|
2696
|
+
createSakanaFuguStaticModel(
|
|
2697
|
+
"fugu-ultra-20260615",
|
|
2698
|
+
"Fugu Ultra 20260615",
|
|
2699
|
+
SAKANA_FUGU_ULTRA_COST,
|
|
2700
|
+
SAKANA_FUGU_ULTRA_CONTEXT_WINDOW,
|
|
2701
|
+
),
|
|
2702
|
+
];
|
|
2703
|
+
|
|
2704
|
+
const SAKANA_FUGU_STATIC_MODEL_BY_ID = new Map(SAKANA_FUGU_STATIC_MODELS.map(model => [model.id, model] as const));
|
|
2705
|
+
const SAKANA_FUGU_STATIC_MODEL_IDS = SAKANA_FUGU_STATIC_MODELS.map(model => model.id);
|
|
2706
|
+
|
|
2707
|
+
export interface SakanaModelManagerConfig {
|
|
2708
|
+
apiKey?: string;
|
|
2709
|
+
baseUrl?: string;
|
|
2710
|
+
fetch?: FetchImpl;
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2713
|
+
export function sakanaModelManagerOptions(config?: SakanaModelManagerConfig): ModelManagerOptions<"openai-responses"> {
|
|
2714
|
+
const apiKey = config?.apiKey;
|
|
2715
|
+
const baseUrl = normalizeSakanaBaseUrl(config?.baseUrl ?? Bun.env.SAKANA_BASE_URL ?? Bun.env.FUGU_BASE_URL);
|
|
2716
|
+
const references = createBundledReferenceMap<"openai-responses">("sakana");
|
|
2717
|
+
return {
|
|
2718
|
+
providerId: "sakana",
|
|
2719
|
+
dynamicModelsAuthoritative: true,
|
|
2720
|
+
dropCachedModelIdsOnStaticMismatch: SAKANA_FUGU_STATIC_MODEL_IDS,
|
|
2721
|
+
...(apiKey && {
|
|
2722
|
+
fetchDynamicModels: () =>
|
|
2723
|
+
fetchOpenAICompatibleModels({
|
|
2724
|
+
api: "openai-responses",
|
|
2725
|
+
provider: "sakana",
|
|
2726
|
+
baseUrl,
|
|
2727
|
+
apiKey,
|
|
2728
|
+
mapModel: (entry, defaults) => {
|
|
2729
|
+
const reference = references.get(defaults.id) ?? SAKANA_FUGU_STATIC_MODEL_BY_ID.get(defaults.id);
|
|
2730
|
+
const model = mapWithBundledReference(entry, defaults, reference);
|
|
2731
|
+
if (!reference && isSakanaFuguModelId(model.id)) {
|
|
2732
|
+
return {
|
|
2733
|
+
...model,
|
|
2734
|
+
reasoning: true,
|
|
2735
|
+
thinking: { ...SAKANA_FUGU_THINKING },
|
|
2736
|
+
compat: { ...SAKANA_RESPONSES_COMPAT },
|
|
2737
|
+
};
|
|
2738
|
+
}
|
|
2739
|
+
return model;
|
|
2740
|
+
},
|
|
2741
|
+
fetch: config?.fetch,
|
|
2742
|
+
}),
|
|
2743
|
+
}),
|
|
2744
|
+
};
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
// ---------------------------------------------------------------------------
|
|
2748
|
+
// 17. Qwen Portal
|
|
2749
|
+
// ---------------------------------------------------------------------------
|
|
2750
|
+
|
|
2751
|
+
export interface QwenPortalModelManagerConfig {
|
|
2752
|
+
apiKey?: string;
|
|
2753
|
+
baseUrl?: string;
|
|
2754
|
+
fetch?: FetchImpl;
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
export function qwenPortalModelManagerOptions(
|
|
2758
|
+
config?: QwenPortalModelManagerConfig,
|
|
2759
|
+
): ModelManagerOptions<"openai-completions"> {
|
|
2760
|
+
return createSimpleOpenAICompletionsOptions("qwen-portal", "https://portal.qwen.ai/v1", config);
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
// ---------------------------------------------------------------------------
|
|
2764
|
+
// 18. Qianfan
|
|
2765
|
+
// ---------------------------------------------------------------------------
|
|
2766
|
+
|
|
2767
|
+
export interface QianfanModelManagerConfig {
|
|
2768
|
+
apiKey?: string;
|
|
2769
|
+
baseUrl?: string;
|
|
2770
|
+
fetch?: FetchImpl;
|
|
2771
|
+
}
|
|
2772
|
+
|
|
2773
|
+
export function qianfanModelManagerOptions(
|
|
2774
|
+
config?: QianfanModelManagerConfig,
|
|
2775
|
+
): ModelManagerOptions<"openai-completions"> {
|
|
2776
|
+
return createSimpleOpenAICompletionsOptions("qianfan", "https://qianfan.baidubce.com/v2", config);
|
|
2777
|
+
}
|
|
2778
|
+
|
|
2779
|
+
// ---------------------------------------------------------------------------
|
|
2780
|
+
// 19. Cloudflare AI Gateway
|
|
2781
|
+
// ---------------------------------------------------------------------------
|
|
2782
|
+
|
|
2783
|
+
export interface CloudflareAiGatewayModelManagerConfig {
|
|
2784
|
+
apiKey?: string;
|
|
2785
|
+
baseUrl?: string;
|
|
2786
|
+
fetch?: FetchImpl;
|
|
2787
|
+
}
|
|
2788
|
+
|
|
2789
|
+
export function cloudflareAiGatewayModelManagerOptions(
|
|
2790
|
+
config?: CloudflareAiGatewayModelManagerConfig,
|
|
2791
|
+
): ModelManagerOptions<"anthropic-messages"> {
|
|
2792
|
+
return createSimpleAnthropicProviderOptions(
|
|
2793
|
+
"cloudflare-ai-gateway",
|
|
2794
|
+
"https://gateway.ai.cloudflare.com/v1/<account>/<gateway>/anthropic",
|
|
2795
|
+
config,
|
|
2796
|
+
);
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2799
|
+
// ---------------------------------------------------------------------------
|
|
2800
|
+
// 20. Xiaomi
|
|
2801
|
+
// ---------------------------------------------------------------------------
|
|
2802
|
+
|
|
2803
|
+
/** Region codes for Xiaomi Token Plan clusters exposed as separate login providers. */
|
|
2804
|
+
export type XiaomiTokenPlanRegion = "sgp" | "ams" | "cn";
|
|
2805
|
+
|
|
2806
|
+
/** Configures Xiaomi standard or regional Token Plan OpenAI-compatible model discovery. */
|
|
2807
|
+
export interface XiaomiModelManagerConfig {
|
|
2808
|
+
apiKey?: string;
|
|
2809
|
+
baseUrl?: string;
|
|
2810
|
+
fetch?: FetchImpl;
|
|
2811
|
+
providerId?: Provider;
|
|
2812
|
+
tokenPlanRegion?: XiaomiTokenPlanRegion;
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2815
|
+
const XIAOMI_TOKEN_PLAN_BASE_URLS: Record<XiaomiTokenPlanRegion, string> = {
|
|
2816
|
+
sgp: "https://token-plan-sgp.xiaomimimo.com/v1",
|
|
2817
|
+
ams: "https://token-plan-ams.xiaomimimo.com/v1",
|
|
2818
|
+
cn: "https://token-plan-cn.xiaomimimo.com/v1",
|
|
2819
|
+
};
|
|
2820
|
+
|
|
2821
|
+
const XIAOMI_TOKEN_PLAN_FALLBACK_BASE_URLS = [
|
|
2822
|
+
XIAOMI_TOKEN_PLAN_BASE_URLS.sgp,
|
|
2823
|
+
XIAOMI_TOKEN_PLAN_BASE_URLS.ams,
|
|
2824
|
+
XIAOMI_TOKEN_PLAN_BASE_URLS.cn,
|
|
2825
|
+
];
|
|
2826
|
+
|
|
2827
|
+
/** Builds a Xiaomi model manager, preserving Token Plan region provider ids during discovery. */
|
|
2828
|
+
export function xiaomiModelManagerOptions(
|
|
2829
|
+
config?: XiaomiModelManagerConfig,
|
|
2830
|
+
): ModelManagerOptions<"openai-completions"> {
|
|
2831
|
+
const apiKey = config?.apiKey;
|
|
2832
|
+
const providerId = config?.providerId ?? "xiaomi";
|
|
2833
|
+
const tokenPlanBaseUrls = config?.tokenPlanRegion
|
|
2834
|
+
? [XIAOMI_TOKEN_PLAN_BASE_URLS[config.tokenPlanRegion]]
|
|
2835
|
+
: XIAOMI_TOKEN_PLAN_FALLBACK_BASE_URLS;
|
|
2836
|
+
const XIAOMI_STANDARD_BASE_URL = "https://api.xiaomimimo.com/v1";
|
|
2837
|
+
const isTokenPlanProvider = config?.tokenPlanRegion !== undefined || providerId.startsWith("xiaomi-token-plan-");
|
|
2838
|
+
const isTokenPlanKey = isTokenPlanProvider || apiKey?.startsWith("tp-");
|
|
2839
|
+
// Token-plan keys always use a TP cluster; config?.baseUrl (from catalog)
|
|
2840
|
+
// would incorrectly pin to the standard endpoint (api.xiaomimimo.com).
|
|
2841
|
+
const baseUrl = isTokenPlanKey ? tokenPlanBaseUrls[0] : (config?.baseUrl ?? XIAOMI_STANDARD_BASE_URL);
|
|
2842
|
+
const references = createBundledReferenceMap<"openai-completions">("xiaomi");
|
|
2843
|
+
const fetchModels = (url: string) =>
|
|
2844
|
+
fetchOpenAICompatibleModels({
|
|
2845
|
+
api: "openai-completions",
|
|
2846
|
+
provider: providerId,
|
|
2847
|
+
baseUrl: url,
|
|
2848
|
+
apiKey,
|
|
2849
|
+
filterModel: (_entry, model) => !model.id.includes("-tts") && !model.id.includes("-asr"),
|
|
2850
|
+
mapModel: (entry, defaults) => {
|
|
2851
|
+
const reference = references.get(defaults.id);
|
|
2852
|
+
const model = mapWithBundledReference(entry, defaults, reference);
|
|
2853
|
+
return {
|
|
2854
|
+
...model,
|
|
2855
|
+
api: "openai-completions",
|
|
2856
|
+
provider: providerId,
|
|
2857
|
+
baseUrl: defaults.baseUrl,
|
|
2858
|
+
name: toModelName(entry.display_name, model.name),
|
|
2859
|
+
};
|
|
2860
|
+
},
|
|
2861
|
+
fetch: config?.fetch,
|
|
2862
|
+
});
|
|
2863
|
+
return {
|
|
2864
|
+
providerId,
|
|
2865
|
+
...(apiKey && {
|
|
2866
|
+
fetchDynamicModels: async () => {
|
|
2867
|
+
if (!isTokenPlanKey) {
|
|
2868
|
+
return fetchModels(baseUrl);
|
|
2869
|
+
}
|
|
2870
|
+
for (const url of tokenPlanBaseUrls) {
|
|
2871
|
+
const result = await fetchModels(url);
|
|
2872
|
+
if (result) return result;
|
|
2873
|
+
}
|
|
2874
|
+
return null;
|
|
2875
|
+
},
|
|
2876
|
+
}),
|
|
2877
|
+
};
|
|
2878
|
+
}
|
|
2879
|
+
// ---------------------------------------------------------------------------
|
|
2880
|
+
// 21. LiteLLM
|
|
2881
|
+
// ---------------------------------------------------------------------------
|
|
2882
|
+
|
|
2883
|
+
export interface LiteLLMModelManagerConfig {
|
|
2884
|
+
apiKey?: string;
|
|
2885
|
+
baseUrl?: string;
|
|
2886
|
+
fetch?: FetchImpl;
|
|
2887
|
+
}
|
|
2888
|
+
|
|
2889
|
+
export interface FetchLiteLLMRichModelsOptions<TApi extends Api> {
|
|
2890
|
+
api: TApi;
|
|
2891
|
+
provider: Provider;
|
|
2892
|
+
baseUrl: string;
|
|
2893
|
+
apiKey?: string;
|
|
2894
|
+
headers?: Record<string, string>;
|
|
2895
|
+
fetch?: FetchImpl;
|
|
2896
|
+
signal?: AbortSignal;
|
|
2897
|
+
timeoutMs?: number;
|
|
2898
|
+
referenceResolver?: (modelId: string) => ModelSpec<TApi> | undefined;
|
|
2899
|
+
}
|
|
2900
|
+
|
|
2901
|
+
type LiteLLMRichModelEntry = Record<string, unknown>;
|
|
2902
|
+
|
|
2903
|
+
const LITELLM_RICH_ENDPOINTS = ["/model_group/info", "/v2/model/info", "/model/info", "/v1/model/info"] as const;
|
|
2904
|
+
export const OPENAI_COMPAT_DISCOVERY_DEFAULT_CONTEXT_WINDOW = 128_000;
|
|
2905
|
+
export const OPENAI_COMPAT_DISCOVERY_DEFAULT_MAX_TOKENS = 32_768;
|
|
2906
|
+
const UNKNOWN_PROXY_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } as const;
|
|
2907
|
+
|
|
2908
|
+
export function normalizeLiteLLMManagementBaseUrl(baseUrl: string): string {
|
|
2909
|
+
const trimmed = baseUrl.trim().replace(/\/+$/g, "");
|
|
2910
|
+
if (!trimmed) {
|
|
2911
|
+
return "";
|
|
2912
|
+
}
|
|
2913
|
+
try {
|
|
2914
|
+
const parsed = new URL(trimmed);
|
|
2915
|
+
const path = parsed.pathname.replace(/\/+$/g, "");
|
|
2916
|
+
parsed.pathname = path.endsWith("/v1") ? path.slice(0, -3) || "/" : path || "/";
|
|
2917
|
+
const normalized = `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
|
|
2918
|
+
return normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
|
|
2919
|
+
} catch {
|
|
2920
|
+
return trimmed.replace(/\/v1$/, "");
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
|
|
2924
|
+
function normalizeLiteLLMRuntimeBaseUrl(baseUrl: string): string {
|
|
2925
|
+
const trimmed = baseUrl.trim();
|
|
2926
|
+
return trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed;
|
|
2927
|
+
}
|
|
2928
|
+
|
|
2929
|
+
const LITELLM_RESELLER_USAGE_SUFFIX = /\s+\(\d+(?:\.\d+)?[x×] usage\)$/i;
|
|
2930
|
+
|
|
2931
|
+
function stripLiteLLMResellerUsageSuffix(name: string): string {
|
|
2932
|
+
const cleaned = name.replace(LITELLM_RESELLER_USAGE_SUFFIX, "").trim();
|
|
2933
|
+
return cleaned.length > 0 ? cleaned : name;
|
|
2934
|
+
}
|
|
2935
|
+
|
|
2936
|
+
function toLiteLLMDisplayName(modelName: string | undefined, referenceName: string | undefined, id: string): string {
|
|
2937
|
+
const cleanedModelName = modelName ? stripLiteLLMResellerUsageSuffix(modelName) : undefined;
|
|
2938
|
+
if (cleanedModelName && cleanedModelName !== id) {
|
|
2939
|
+
return cleanedModelName;
|
|
2940
|
+
}
|
|
2941
|
+
return referenceName ? stripLiteLLMResellerUsageSuffix(referenceName) : id;
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2944
|
+
function mapLiteLLMOpenAICompatibleModel<TApi extends Api>(
|
|
2945
|
+
entry: OpenAICompatibleModelRecord,
|
|
2946
|
+
defaults: ModelSpec<TApi>,
|
|
2947
|
+
reference: ModelSpec<TApi> | undefined,
|
|
2948
|
+
): ModelSpec<TApi> {
|
|
2949
|
+
const model = mapWithBundledReference(entry, defaults, reference);
|
|
2950
|
+
return {
|
|
2951
|
+
...model,
|
|
2952
|
+
name: stripLiteLLMResellerUsageSuffix(model.name),
|
|
2953
|
+
};
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2956
|
+
function toNonEmptyString(value: unknown): string | undefined {
|
|
2957
|
+
if (typeof value !== "string") {
|
|
2958
|
+
return undefined;
|
|
2959
|
+
}
|
|
2960
|
+
const trimmed = value.trim();
|
|
2961
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
2962
|
+
}
|
|
2963
|
+
|
|
2964
|
+
function extractLiteLLMRichEntries(payload: unknown): LiteLLMRichModelEntry[] | null {
|
|
2965
|
+
if (Array.isArray(payload)) {
|
|
2966
|
+
return payload.flatMap(entry => (isRecord(entry) ? [entry] : []));
|
|
2967
|
+
}
|
|
2968
|
+
if (!isRecord(payload)) {
|
|
2969
|
+
return null;
|
|
2970
|
+
}
|
|
2971
|
+
for (const candidate of [payload.data, payload.models, payload.result, payload.items]) {
|
|
2972
|
+
if (candidate === undefined) {
|
|
2973
|
+
continue;
|
|
2974
|
+
}
|
|
2975
|
+
const entries = extractLiteLLMRichEntries(candidate);
|
|
2976
|
+
if (entries !== null) {
|
|
2977
|
+
return entries;
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
return null;
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2983
|
+
function getLiteLLMModelInfo(entry: LiteLLMRichModelEntry): LiteLLMRichModelEntry | undefined {
|
|
2984
|
+
return isRecord(entry.model_info) ? entry.model_info : undefined;
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2987
|
+
function getLiteLLMParams(entry: LiteLLMRichModelEntry): LiteLLMRichModelEntry | undefined {
|
|
2988
|
+
return isRecord(entry.litellm_params) ? entry.litellm_params : undefined;
|
|
2989
|
+
}
|
|
2990
|
+
|
|
2991
|
+
function getLiteLLMMetadataValue(entry: LiteLLMRichModelEntry, key: string): unknown {
|
|
2992
|
+
return entry[key] ?? getLiteLLMModelInfo(entry)?.[key];
|
|
2993
|
+
}
|
|
2994
|
+
|
|
2995
|
+
function getLiteLLMRichModelId(entry: LiteLLMRichModelEntry): string | undefined {
|
|
2996
|
+
return (
|
|
2997
|
+
toNonEmptyString(entry.model_group) ??
|
|
2998
|
+
toNonEmptyString(entry.model_name) ??
|
|
2999
|
+
toNonEmptyString(entry.id) ??
|
|
3000
|
+
toNonEmptyString(getLiteLLMParams(entry)?.model)
|
|
3001
|
+
);
|
|
3002
|
+
}
|
|
3003
|
+
|
|
3004
|
+
function getSupportedOpenAIParams(entry: LiteLLMRichModelEntry): string[] | undefined {
|
|
3005
|
+
const value = getLiteLLMMetadataValue(entry, "supported_openai_params");
|
|
3006
|
+
if (!Array.isArray(value)) {
|
|
3007
|
+
return undefined;
|
|
3008
|
+
}
|
|
3009
|
+
return value.flatMap(item => (typeof item === "string" ? [item] : []));
|
|
3010
|
+
}
|
|
3011
|
+
|
|
3012
|
+
function mapLiteLLMRichEntry<TApi extends Api>(
|
|
3013
|
+
entry: LiteLLMRichModelEntry,
|
|
3014
|
+
options: FetchLiteLLMRichModelsOptions<TApi>,
|
|
3015
|
+
runtimeBaseUrl: string,
|
|
3016
|
+
): ModelSpec<TApi> | null {
|
|
3017
|
+
const id = getLiteLLMRichModelId(entry);
|
|
3018
|
+
if (!id) {
|
|
3019
|
+
return null;
|
|
3020
|
+
}
|
|
3021
|
+
const reference = options.referenceResolver?.(id);
|
|
3022
|
+
const modelName = toNonEmptyString(entry.model_name);
|
|
3023
|
+
const contextWindow = toPositiveNumber(
|
|
3024
|
+
getLiteLLMMetadataValue(entry, "max_input_tokens"),
|
|
3025
|
+
reference?.contextWindow ?? OPENAI_COMPAT_DISCOVERY_DEFAULT_CONTEXT_WINDOW,
|
|
3026
|
+
);
|
|
3027
|
+
const maxTokens = toPositiveNumber(
|
|
3028
|
+
getLiteLLMMetadataValue(entry, "max_output_tokens"),
|
|
3029
|
+
reference?.maxTokens ?? Math.min(contextWindow, OPENAI_COMPAT_DISCOVERY_DEFAULT_MAX_TOKENS),
|
|
3030
|
+
);
|
|
3031
|
+
const supportsVision = getLiteLLMMetadataValue(entry, "supports_vision");
|
|
3032
|
+
const supportsReasoning = getLiteLLMMetadataValue(entry, "supports_reasoning");
|
|
3033
|
+
const supportedOpenAIParams = getSupportedOpenAIParams(entry);
|
|
3034
|
+
const supportsFunctionCalling = getLiteLLMMetadataValue(entry, "supports_function_calling");
|
|
3035
|
+
const supportsTools =
|
|
3036
|
+
supportsFunctionCalling === true
|
|
3037
|
+
? true
|
|
3038
|
+
: supportsFunctionCalling === false
|
|
3039
|
+
? false
|
|
3040
|
+
: supportedOpenAIParams !== undefined
|
|
3041
|
+
? supportedOpenAIParams.some(param =>
|
|
3042
|
+
["tools", "tool_choice", "functions", "function_call"].includes(param),
|
|
3043
|
+
)
|
|
3044
|
+
: reference?.supportsTools;
|
|
3045
|
+
const compat: OpenAICompat = {
|
|
3046
|
+
...(reference?.compat ?? {}),
|
|
3047
|
+
supportsStore: false,
|
|
3048
|
+
supportsDeveloperRole: false,
|
|
3049
|
+
...(supportedOpenAIParams !== undefined
|
|
3050
|
+
? { supportsReasoningEffort: supportedOpenAIParams.includes("reasoning_effort") }
|
|
3051
|
+
: {}),
|
|
3052
|
+
};
|
|
3053
|
+
return {
|
|
3054
|
+
id,
|
|
3055
|
+
name: toLiteLLMDisplayName(modelName, reference?.name, id),
|
|
3056
|
+
api: options.api,
|
|
3057
|
+
provider: options.provider,
|
|
3058
|
+
baseUrl: runtimeBaseUrl,
|
|
3059
|
+
contextWindow,
|
|
3060
|
+
maxTokens,
|
|
3061
|
+
input:
|
|
3062
|
+
supportsVision === true
|
|
3063
|
+
? ["text", "image"]
|
|
3064
|
+
: supportsVision === false
|
|
3065
|
+
? ["text"]
|
|
3066
|
+
: (reference?.input ?? ["text"]),
|
|
3067
|
+
reasoning: typeof supportsReasoning === "boolean" ? supportsReasoning : (reference?.reasoning ?? false),
|
|
3068
|
+
thinking: reference?.thinking,
|
|
3069
|
+
cost: reference?.cost ?? UNKNOWN_PROXY_COST,
|
|
3070
|
+
...(supportsTools !== undefined ? { supportsTools } : {}),
|
|
3071
|
+
compat: compat as ModelSpec<TApi>["compat"],
|
|
3072
|
+
};
|
|
3073
|
+
}
|
|
3074
|
+
|
|
3075
|
+
async function fetchLiteLLMRichEndpoint<TApi extends Api>(
|
|
3076
|
+
endpoint: string,
|
|
3077
|
+
options: FetchLiteLLMRichModelsOptions<TApi>,
|
|
3078
|
+
managementBaseUrl: string,
|
|
3079
|
+
runtimeBaseUrl: string,
|
|
3080
|
+
signal?: AbortSignal,
|
|
3081
|
+
): Promise<ModelSpec<TApi>[] | null> {
|
|
3082
|
+
const fetchImpl = discoveryFetch(options.fetch);
|
|
3083
|
+
const requestHeaders: Record<string, string> = {
|
|
3084
|
+
Accept: "application/json",
|
|
3085
|
+
...options.headers,
|
|
3086
|
+
};
|
|
3087
|
+
if (options.apiKey) {
|
|
3088
|
+
requestHeaders.Authorization = `Bearer ${options.apiKey}`;
|
|
3089
|
+
}
|
|
3090
|
+
let response: Response;
|
|
3091
|
+
try {
|
|
3092
|
+
response = await fetchImpl(`${managementBaseUrl}${endpoint}`, {
|
|
3093
|
+
method: "GET",
|
|
3094
|
+
headers: requestHeaders,
|
|
3095
|
+
signal,
|
|
3096
|
+
});
|
|
3097
|
+
} catch {
|
|
3098
|
+
return null;
|
|
3099
|
+
}
|
|
3100
|
+
if (!response.ok) {
|
|
3101
|
+
return null;
|
|
3102
|
+
}
|
|
3103
|
+
let payload: unknown;
|
|
3104
|
+
try {
|
|
3105
|
+
payload = await response.json();
|
|
3106
|
+
} catch {
|
|
3107
|
+
return null;
|
|
3108
|
+
}
|
|
3109
|
+
const entries = extractLiteLLMRichEntries(payload);
|
|
3110
|
+
if (!entries || entries.length === 0) {
|
|
3111
|
+
return null;
|
|
3112
|
+
}
|
|
3113
|
+
const deduped = new Map<string, ModelSpec<TApi>>();
|
|
3114
|
+
for (const entry of entries) {
|
|
3115
|
+
const model = mapLiteLLMRichEntry(entry, options, runtimeBaseUrl);
|
|
3116
|
+
if (model) {
|
|
3117
|
+
deduped.set(model.id, model);
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
if (deduped.size === 0) {
|
|
3121
|
+
return null;
|
|
3122
|
+
}
|
|
3123
|
+
return Array.from(deduped.values()).sort((left, right) => left.id.localeCompare(right.id));
|
|
3124
|
+
}
|
|
3125
|
+
|
|
3126
|
+
export async function fetchLiteLLMRichModels<TApi extends Api>(
|
|
3127
|
+
options: FetchLiteLLMRichModelsOptions<TApi>,
|
|
3128
|
+
): Promise<ModelSpec<TApi>[] | null> {
|
|
3129
|
+
const managementBaseUrl = normalizeLiteLLMManagementBaseUrl(options.baseUrl);
|
|
3130
|
+
const runtimeBaseUrl = normalizeLiteLLMRuntimeBaseUrl(options.baseUrl);
|
|
3131
|
+
if (!managementBaseUrl || !runtimeBaseUrl) {
|
|
3132
|
+
return null;
|
|
3133
|
+
}
|
|
3134
|
+
const fetchModels = async (signal?: AbortSignal): Promise<ModelSpec<TApi>[] | null> => {
|
|
3135
|
+
for (const endpoint of LITELLM_RICH_ENDPOINTS) {
|
|
3136
|
+
const models = await fetchLiteLLMRichEndpoint(endpoint, options, managementBaseUrl, runtimeBaseUrl, signal);
|
|
3137
|
+
if (models) {
|
|
3138
|
+
return models;
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
return null;
|
|
3142
|
+
};
|
|
3143
|
+
if (options.signal !== undefined) {
|
|
3144
|
+
return fetchModels(options.signal);
|
|
3145
|
+
}
|
|
3146
|
+
return options.timeoutMs !== undefined ? withCatalogDiscoveryTimeout(options.timeoutMs, fetchModels) : fetchModels();
|
|
3147
|
+
}
|
|
3148
|
+
|
|
3149
|
+
export function litellmModelManagerOptions(
|
|
3150
|
+
config?: LiteLLMModelManagerConfig,
|
|
3151
|
+
): ModelManagerOptions<"openai-completions"> {
|
|
3152
|
+
const apiKey = config?.apiKey;
|
|
3153
|
+
const baseUrl = config?.baseUrl ?? Bun.env.LITELLM_BASE_URL ?? "http://localhost:4000/v1";
|
|
3154
|
+
return {
|
|
3155
|
+
providerId: "litellm",
|
|
3156
|
+
// rich-v2 invalidates rows cached before reseller usage-suffix stripping
|
|
3157
|
+
// (stale display names like `MiniMax-M3 (3x usage)`); bump the version
|
|
3158
|
+
// whenever the mappers below change, or warm authoritative caches keep
|
|
3159
|
+
// serving pre-change rows for the full TTL.
|
|
3160
|
+
cacheProviderId: `litellm:rich-v2:${Bun.hash(baseUrl).toString(36)}`,
|
|
3161
|
+
// litellm is a local-only proxy and is never bundled in models.json (that
|
|
3162
|
+
// would leak the machine's localhost catalog). Prefer the proxy's richer
|
|
3163
|
+
// management metadata, then fall back to /v1/models and enrich bare ids
|
|
3164
|
+
// against models.dev like the gateway providers (fireworks et al.) do.
|
|
3165
|
+
fetchDynamicModels: async () => {
|
|
3166
|
+
const modelsDevReferences = await loadModelsDevReferences<"openai-completions">(config?.fetch);
|
|
3167
|
+
const resolveReference = (id: string) => modelsDevReferences.get(id);
|
|
3168
|
+
const richModels = await fetchLiteLLMRichModels({
|
|
3169
|
+
api: "openai-completions",
|
|
3170
|
+
provider: "litellm",
|
|
3171
|
+
baseUrl,
|
|
3172
|
+
apiKey,
|
|
3173
|
+
fetch: config?.fetch,
|
|
3174
|
+
referenceResolver: resolveReference,
|
|
3175
|
+
timeoutMs: 10_000,
|
|
3176
|
+
});
|
|
3177
|
+
if (richModels && richModels.length > 0) {
|
|
3178
|
+
return richModels;
|
|
3179
|
+
}
|
|
3180
|
+
return fetchOpenAICompatibleModels({
|
|
3181
|
+
api: "openai-completions",
|
|
3182
|
+
provider: "litellm",
|
|
3183
|
+
baseUrl,
|
|
3184
|
+
apiKey,
|
|
3185
|
+
mapModel: (entry, defaults) =>
|
|
3186
|
+
mapLiteLLMOpenAICompatibleModel(entry, defaults, resolveReference(defaults.id)),
|
|
3187
|
+
fetch: config?.fetch,
|
|
3188
|
+
});
|
|
3189
|
+
},
|
|
3190
|
+
};
|
|
3191
|
+
}
|
|
3192
|
+
|
|
3193
|
+
// ---------------------------------------------------------------------------
|
|
3194
|
+
// 22. vLLM
|
|
3195
|
+
// ---------------------------------------------------------------------------
|
|
3196
|
+
|
|
3197
|
+
const VLLM_DISCOVERY_TIMEOUT_MS = 10_000;
|
|
3198
|
+
|
|
3199
|
+
export interface VllmModelManagerConfig {
|
|
3200
|
+
apiKey?: string;
|
|
3201
|
+
baseUrl?: string;
|
|
3202
|
+
fetch?: FetchImpl;
|
|
3203
|
+
}
|
|
3204
|
+
|
|
3205
|
+
export function vllmModelManagerOptions(config?: VllmModelManagerConfig): ModelManagerOptions<"openai-completions"> {
|
|
3206
|
+
const apiKey = config?.apiKey;
|
|
3207
|
+
const baseUrl = config?.baseUrl ?? "http://127.0.0.1:8000/v1";
|
|
3208
|
+
const references = createBundledReferenceMap<"openai-completions">("vllm" as Parameters<typeof getBundledModels>[0]);
|
|
3209
|
+
return {
|
|
3210
|
+
providerId: "vllm",
|
|
3211
|
+
cacheProviderId: `vllm:${Bun.hash(baseUrl).toString(36)}`,
|
|
3212
|
+
fetchDynamicModels: () =>
|
|
3213
|
+
fetchOpenAICompatibleModels({
|
|
3214
|
+
api: "openai-completions",
|
|
3215
|
+
provider: "vllm",
|
|
3216
|
+
baseUrl,
|
|
3217
|
+
apiKey,
|
|
3218
|
+
mapModel: (entry, defaults) => {
|
|
3219
|
+
const model = mapWithBundledReference(entry, defaults, references.get(defaults.id));
|
|
3220
|
+
return {
|
|
3221
|
+
...model,
|
|
3222
|
+
contextWindow: toPositiveNumber(entry.max_model_len, model.contextWindow),
|
|
3223
|
+
};
|
|
3224
|
+
},
|
|
3225
|
+
fetch: config?.fetch,
|
|
3226
|
+
timeoutMs: VLLM_DISCOVERY_TIMEOUT_MS,
|
|
3227
|
+
}),
|
|
3228
|
+
};
|
|
3229
|
+
}
|
|
3230
|
+
|
|
3231
|
+
// ---------------------------------------------------------------------------
|
|
3232
|
+
// 23. NanoGPT
|
|
3233
|
+
// ---------------------------------------------------------------------------
|
|
3234
|
+
|
|
3235
|
+
export interface NanoGptModelManagerConfig {
|
|
3236
|
+
apiKey?: string;
|
|
3237
|
+
baseUrl?: string;
|
|
3238
|
+
fetch?: FetchImpl;
|
|
3239
|
+
}
|
|
3240
|
+
|
|
3241
|
+
export function nanoGptModelManagerOptions(
|
|
3242
|
+
config?: NanoGptModelManagerConfig,
|
|
3243
|
+
): ModelManagerOptions<"openai-completions"> {
|
|
3244
|
+
const apiKey = config?.apiKey;
|
|
3245
|
+
const baseUrl = config?.baseUrl ?? "https://nano-gpt.com/api/v1";
|
|
3246
|
+
const resolveReference = createReferenceResolver(
|
|
3247
|
+
createBundledReferenceMap<"openai-completions">("nanogpt" as Parameters<typeof getBundledModels>[0]),
|
|
3248
|
+
);
|
|
3249
|
+
return {
|
|
3250
|
+
providerId: "nanogpt",
|
|
3251
|
+
...(apiKey && {
|
|
3252
|
+
fetchDynamicModels: async () => {
|
|
3253
|
+
// Track base IDs that have :thinking variants so we can mark them reasoning-capable.
|
|
3254
|
+
const thinkingBaseIds = new Set<string>();
|
|
3255
|
+
const models = await fetchOpenAICompatibleModels({
|
|
3256
|
+
api: "openai-completions",
|
|
3257
|
+
provider: "nanogpt",
|
|
3258
|
+
baseUrl,
|
|
3259
|
+
apiKey,
|
|
3260
|
+
mapModel: (entry, defaults) => {
|
|
3261
|
+
const reference = resolveReference(defaults.id);
|
|
3262
|
+
const mapped = mapWithBundledReference(entry, defaults, reference);
|
|
3263
|
+
return { ...mapped, api: "openai-completions", provider: "nanogpt" };
|
|
3264
|
+
},
|
|
3265
|
+
filterModel: (_entry, model) => {
|
|
3266
|
+
const match = NANO_GPT_THINKING_SUFFIX_RE.exec(model.id);
|
|
3267
|
+
if (match) {
|
|
3268
|
+
thinkingBaseIds.add(model.id.slice(0, match.index));
|
|
3269
|
+
return false;
|
|
3270
|
+
}
|
|
3271
|
+
return isLikelyNanoGptTextModelId(model.id);
|
|
3272
|
+
},
|
|
3273
|
+
fetch: config?.fetch,
|
|
3274
|
+
});
|
|
3275
|
+
if (!models) return null;
|
|
3276
|
+
// Mark base models as reasoning-capable when a :thinking variant existed.
|
|
3277
|
+
for (const model of models) {
|
|
3278
|
+
if (!model.reasoning && thinkingBaseIds.has(model.id)) {
|
|
3279
|
+
(model as { reasoning: boolean }).reasoning = true;
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
return models;
|
|
3283
|
+
},
|
|
3284
|
+
}),
|
|
3285
|
+
};
|
|
3286
|
+
}
|
|
3287
|
+
|
|
3288
|
+
// ---------------------------------------------------------------------------
|
|
3289
|
+
// 24. GitHub Copilot
|
|
3290
|
+
// ---------------------------------------------------------------------------
|
|
3291
|
+
|
|
3292
|
+
export interface GithubCopilotModelManagerConfig {
|
|
3293
|
+
apiKey?: string;
|
|
3294
|
+
baseUrl?: string;
|
|
3295
|
+
fetch?: FetchImpl;
|
|
3296
|
+
}
|
|
3297
|
+
|
|
3298
|
+
const COPILOT_ANTHROPIC_MODEL_PATTERN = /^claude-(haiku|sonnet|opus|fable|mythos)-\d/;
|
|
3299
|
+
const isCopilotResponsesModelId = (modelId: string): boolean =>
|
|
3300
|
+
modelId.startsWith("gpt-5") || modelId.startsWith("oswe");
|
|
3301
|
+
|
|
3302
|
+
function inferCopilotApi(modelId: string): Api {
|
|
3303
|
+
if (COPILOT_ANTHROPIC_MODEL_PATTERN.test(modelId)) {
|
|
3304
|
+
return "anthropic-messages";
|
|
3305
|
+
}
|
|
3306
|
+
if (isCopilotResponsesModelId(modelId)) {
|
|
3307
|
+
return "openai-responses";
|
|
3308
|
+
}
|
|
3309
|
+
return "openai-completions";
|
|
3310
|
+
}
|
|
3311
|
+
|
|
3312
|
+
function extractCopilotLimits(entry: OpenAICompatibleModelRecord): {
|
|
3313
|
+
maxPromptTokens?: number;
|
|
3314
|
+
maxContextWindowTokens?: number;
|
|
3315
|
+
maxOutputTokens?: number;
|
|
3316
|
+
maxNonStreamingOutputTokens?: number;
|
|
3317
|
+
} {
|
|
3318
|
+
if (!isRecord(entry.capabilities)) {
|
|
3319
|
+
return {};
|
|
3320
|
+
}
|
|
3321
|
+
const limitsValue = entry.capabilities.limits;
|
|
3322
|
+
if (!isRecord(limitsValue)) {
|
|
3323
|
+
return {};
|
|
3324
|
+
}
|
|
3325
|
+
return {
|
|
3326
|
+
maxPromptTokens: toNumber(limitsValue.max_prompt_tokens),
|
|
3327
|
+
maxContextWindowTokens: toNumber(limitsValue.max_context_window_tokens),
|
|
3328
|
+
maxOutputTokens: toNumber(limitsValue.max_output_tokens),
|
|
3329
|
+
maxNonStreamingOutputTokens: toNumber(limitsValue.max_non_streaming_output_tokens),
|
|
3330
|
+
};
|
|
3331
|
+
}
|
|
3332
|
+
|
|
3333
|
+
/** Local id/name suffixes for synthesized Copilot long-context variants. */
|
|
3334
|
+
export const COPILOT_LONG_CONTEXT_ID_SUFFIX = "-1m";
|
|
3335
|
+
const COPILOT_LONG_CONTEXT_NAME_SUFFIX = " (1M)";
|
|
3336
|
+
|
|
3337
|
+
/** One tier of Copilot token pricing (`billing.token_prices.{default,long_context}`). Prices are hundredths of a dollar per 1M tokens. */
|
|
3338
|
+
interface CopilotTokenPriceTier {
|
|
3339
|
+
contextMax?: number;
|
|
3340
|
+
inputPrice?: number;
|
|
3341
|
+
outputPrice?: number;
|
|
3342
|
+
cachePrice?: number;
|
|
3343
|
+
}
|
|
3344
|
+
|
|
3345
|
+
function parseCopilotTokenPriceTier(value: unknown): CopilotTokenPriceTier | undefined {
|
|
3346
|
+
if (!isRecord(value)) {
|
|
3347
|
+
return undefined;
|
|
3348
|
+
}
|
|
3349
|
+
return {
|
|
3350
|
+
contextMax: toNumber(value.context_max),
|
|
3351
|
+
inputPrice: toNumber(value.input_price),
|
|
3352
|
+
outputPrice: toNumber(value.output_price),
|
|
3353
|
+
cachePrice: toNumber(value.cache_price),
|
|
3354
|
+
};
|
|
3355
|
+
}
|
|
3356
|
+
|
|
3357
|
+
/**
|
|
3358
|
+
* Tiered context boundaries/prices from `billing.token_prices`. Served only
|
|
3359
|
+
* when discovery requests `X-GitHub-Api-Version` ≥ 2026-06-01; absent on the
|
|
3360
|
+
* legacy response shape (where `capabilities.limits` is already tier-capped).
|
|
3361
|
+
*/
|
|
3362
|
+
function extractCopilotTokenPrices(entry: OpenAICompatibleModelRecord): {
|
|
3363
|
+
defaultTier?: CopilotTokenPriceTier;
|
|
3364
|
+
longContext?: CopilotTokenPriceTier;
|
|
3365
|
+
} {
|
|
3366
|
+
if (!isRecord(entry.billing)) {
|
|
3367
|
+
return {};
|
|
3368
|
+
}
|
|
3369
|
+
const tokenPrices = entry.billing.token_prices;
|
|
3370
|
+
if (!isRecord(tokenPrices)) {
|
|
3371
|
+
return {};
|
|
3372
|
+
}
|
|
3373
|
+
return {
|
|
3374
|
+
defaultTier: parseCopilotTokenPriceTier(tokenPrices.default),
|
|
3375
|
+
longContext: parseCopilotTokenPriceTier(tokenPrices.long_context),
|
|
3376
|
+
};
|
|
3377
|
+
}
|
|
3378
|
+
|
|
3379
|
+
function extractCopilotSupportsVision(entry: OpenAICompatibleModelRecord): boolean | undefined {
|
|
3380
|
+
if (!isRecord(entry.capabilities)) {
|
|
3381
|
+
return undefined;
|
|
3382
|
+
}
|
|
3383
|
+
const supports = entry.capabilities.supports;
|
|
3384
|
+
if (!isRecord(supports)) {
|
|
3385
|
+
return undefined;
|
|
3386
|
+
}
|
|
3387
|
+
return toBoolean(supports.vision);
|
|
3388
|
+
}
|
|
3389
|
+
|
|
3390
|
+
/** Copilot's `/models` mixes chat and embedding models; only `type: "chat"` entries are usable here. */
|
|
3391
|
+
function isCopilotChatModel(entry: OpenAICompatibleModelRecord): boolean {
|
|
3392
|
+
if (!isRecord(entry.capabilities)) {
|
|
3393
|
+
return true;
|
|
3394
|
+
}
|
|
3395
|
+
const type = entry.capabilities.type;
|
|
3396
|
+
return typeof type !== "string" || type === "chat";
|
|
3397
|
+
}
|
|
3398
|
+
|
|
3399
|
+
function copilotTierCost(
|
|
3400
|
+
tier: CopilotTokenPriceTier | undefined,
|
|
3401
|
+
): Omit<ModelSpec<Api>["cost"], "cacheWrite"> | undefined {
|
|
3402
|
+
if (tier?.inputPrice === undefined || tier.outputPrice === undefined) {
|
|
3403
|
+
return undefined;
|
|
3404
|
+
}
|
|
3405
|
+
return {
|
|
3406
|
+
input: tier.inputPrice / 100,
|
|
3407
|
+
output: tier.outputPrice / 100,
|
|
3408
|
+
cacheRead: (tier.cachePrice ?? 0) / 100,
|
|
3409
|
+
};
|
|
3410
|
+
}
|
|
3411
|
+
|
|
3412
|
+
/**
|
|
3413
|
+
* Synthesize the opt-in long-context sibling for a Copilot model that reports
|
|
3414
|
+
* a `billing.token_prices.long_context` tier (e.g. Claude Opus 200k → 1M, as
|
|
3415
|
+
* selectable in copilot-cli). The variant is a local catalog entry: it keeps
|
|
3416
|
+
* the upstream model id on the wire via `requestModelId` — the tier is purely
|
|
3417
|
+
* a client-side context budget with its own pricing, not a served model id.
|
|
3418
|
+
* The base entry stays on the default tier so nobody silently pays
|
|
3419
|
+
* long-context rates.
|
|
3420
|
+
*/
|
|
3421
|
+
function createCopilotLongContextVariant(
|
|
3422
|
+
base: ModelSpec<Api>,
|
|
3423
|
+
fullContextWindow: number | null,
|
|
3424
|
+
maxTokens: number | null,
|
|
3425
|
+
longContext: CopilotTokenPriceTier | undefined,
|
|
3426
|
+
): ModelSpec<Api> | undefined {
|
|
3427
|
+
const longContextMax = longContext?.contextMax;
|
|
3428
|
+
if (longContextMax === undefined || longContextMax <= 0 || fullContextWindow === null || maxTokens === null) {
|
|
3429
|
+
return undefined;
|
|
3430
|
+
}
|
|
3431
|
+
const variantWindow = Math.min(fullContextWindow, longContextMax + maxTokens);
|
|
3432
|
+
if (base.contextWindow === null || variantWindow <= base.contextWindow) {
|
|
3433
|
+
return undefined;
|
|
3434
|
+
}
|
|
3435
|
+
const longCost = copilotTierCost(longContext);
|
|
3436
|
+
return {
|
|
3437
|
+
...base,
|
|
3438
|
+
id: `${base.id}${COPILOT_LONG_CONTEXT_ID_SUFFIX}`,
|
|
3439
|
+
requestModelId: base.id,
|
|
3440
|
+
name: `${base.name}${COPILOT_LONG_CONTEXT_NAME_SUFFIX}`,
|
|
3441
|
+
contextWindow: variantWindow,
|
|
3442
|
+
// Long-context tier has its own token prices (Gemini/GPT bill ~2x above
|
|
3443
|
+
// the default boundary). cacheWrite is not reported per tier; inherit.
|
|
3444
|
+
...(longCost && { cost: { ...longCost, cacheWrite: base.cost.cacheWrite } }),
|
|
3445
|
+
contextPromotionTarget: undefined,
|
|
3446
|
+
};
|
|
3447
|
+
}
|
|
3448
|
+
|
|
3449
|
+
export function githubCopilotModelManagerOptions(config?: GithubCopilotModelManagerConfig): ModelManagerOptions<Api> {
|
|
3450
|
+
const rawApiKey = config?.apiKey;
|
|
3451
|
+
const configuredBaseUrl = config?.baseUrl ?? "https://api.githubcopilot.com";
|
|
3452
|
+
const parsedApiKey = rawApiKey ? parseGitHubCopilotApiKey(rawApiKey) : undefined;
|
|
3453
|
+
const apiKey = parsedApiKey?.accessToken;
|
|
3454
|
+
const baseUrl =
|
|
3455
|
+
parsedApiKey?.apiEndpoint && configuredBaseUrl.includes("githubcopilot.com")
|
|
3456
|
+
? parsedApiKey.apiEndpoint
|
|
3457
|
+
: parsedApiKey?.enterpriseUrl && configuredBaseUrl.includes("githubcopilot.com")
|
|
3458
|
+
? getGitHubCopilotBaseUrl(parsedApiKey.enterpriseUrl)
|
|
3459
|
+
: configuredBaseUrl;
|
|
3460
|
+
const providerRefs = createBundledReferenceMap<Api>("github-copilot");
|
|
3461
|
+
const resolveReference = createReferenceResolver(providerRefs);
|
|
3462
|
+
return {
|
|
3463
|
+
providerId: "github-copilot",
|
|
3464
|
+
...(apiKey && {
|
|
3465
|
+
fetchDynamicModels: async () => {
|
|
3466
|
+
const longContextVariants: ModelSpec<Api>[] = [];
|
|
3467
|
+
const models = await fetchOpenAICompatibleModels<Api>({
|
|
3468
|
+
api: "openai-completions",
|
|
3469
|
+
provider: "github-copilot",
|
|
3470
|
+
baseUrl,
|
|
3471
|
+
apiKey,
|
|
3472
|
+
headers: COPILOT_API_HEADERS,
|
|
3473
|
+
mapModel: (
|
|
3474
|
+
entry: OpenAICompatibleModelRecord,
|
|
3475
|
+
defaults: ModelSpec<Api>,
|
|
3476
|
+
_context: OpenAICompatibleModelMapperContext<Api>,
|
|
3477
|
+
): ModelSpec<Api> | null => {
|
|
3478
|
+
if (!isCopilotChatModel(entry)) {
|
|
3479
|
+
return null;
|
|
3480
|
+
}
|
|
3481
|
+
const reference = resolveReference(defaults.id);
|
|
3482
|
+
const copilotLimits = extractCopilotLimits(entry);
|
|
3483
|
+
// Copilot exposes token limits under capabilities.limits.*.
|
|
3484
|
+
// max_context_window_tokens is the model's total usable window;
|
|
3485
|
+
// max_prompt_tokens is Copilot's prompt/summarization budget and
|
|
3486
|
+
// must only be a fallback when total-window fields are absent.
|
|
3487
|
+
const contextWindow = toPositiveNumber(
|
|
3488
|
+
copilotLimits.maxContextWindowTokens,
|
|
3489
|
+
toPositiveNumber(
|
|
3490
|
+
entry.context_length,
|
|
3491
|
+
toPositiveNumber(
|
|
3492
|
+
copilotLimits.maxPromptTokens,
|
|
3493
|
+
reference?.contextWindow ?? defaults.contextWindow,
|
|
3494
|
+
),
|
|
3495
|
+
),
|
|
3496
|
+
);
|
|
3497
|
+
const maxTokens = toPositiveNumber(
|
|
3498
|
+
copilotLimits.maxOutputTokens,
|
|
3499
|
+
toPositiveNumber(
|
|
3500
|
+
entry.max_completion_tokens,
|
|
3501
|
+
toPositiveNumber(
|
|
3502
|
+
copilotLimits.maxNonStreamingOutputTokens,
|
|
3503
|
+
reference?.maxTokens ?? defaults.maxTokens,
|
|
3504
|
+
),
|
|
3505
|
+
),
|
|
3506
|
+
);
|
|
3507
|
+
const name =
|
|
3508
|
+
typeof entry.name === "string" && entry.name.trim().length > 0
|
|
3509
|
+
? entry.name
|
|
3510
|
+
: (reference?.name ?? defaults.name);
|
|
3511
|
+
const api = inferCopilotApi(defaults.id);
|
|
3512
|
+
// `supports.vision` reports the model's intrinsic capability, but
|
|
3513
|
+
// the business/enterprise endpoints respond `400 vision is not
|
|
3514
|
+
// supported` on image inputs. Only honour the flag for the
|
|
3515
|
+
// canonical personal-Copilot host.
|
|
3516
|
+
const supportsVision = extractCopilotSupportsVision(entry);
|
|
3517
|
+
const input: ModelSpec<Api>["input"] = isPersonalGitHubCopilotBaseUrl(baseUrl)
|
|
3518
|
+
? supportsVision
|
|
3519
|
+
? ["text", "image"]
|
|
3520
|
+
: (reference?.input ?? defaults.input)
|
|
3521
|
+
: ["text"];
|
|
3522
|
+
// With COPILOT_API_HEADERS the served window is the long-context
|
|
3523
|
+
// ceiling; the default tier ends at token_prices.default.context_max
|
|
3524
|
+
// prompt tokens. Cap the base entry to the default tier — the long
|
|
3525
|
+
// tier is the opt-in `-1m` sibling below.
|
|
3526
|
+
const tokenPrices = extractCopilotTokenPrices(entry);
|
|
3527
|
+
const defaultContextMax = tokenPrices.defaultTier?.contextMax;
|
|
3528
|
+
const defaultTierWindow =
|
|
3529
|
+
defaultContextMax !== undefined &&
|
|
3530
|
+
defaultContextMax > 0 &&
|
|
3531
|
+
contextWindow !== null &&
|
|
3532
|
+
maxTokens !== null
|
|
3533
|
+
? Math.min(contextWindow, defaultContextMax + maxTokens)
|
|
3534
|
+
: contextWindow;
|
|
3535
|
+
const base: ModelSpec<Api> = reference
|
|
3536
|
+
? {
|
|
3537
|
+
...reference,
|
|
3538
|
+
api,
|
|
3539
|
+
provider: "github-copilot",
|
|
3540
|
+
baseUrl,
|
|
3541
|
+
name,
|
|
3542
|
+
input,
|
|
3543
|
+
contextWindow: defaultTierWindow,
|
|
3544
|
+
maxTokens,
|
|
3545
|
+
headers: { ...COPILOT_API_HEADERS, ...(providerRefs.get(defaults.id)?.headers ?? {}) },
|
|
3546
|
+
...(api === "openai-completions"
|
|
3547
|
+
? {
|
|
3548
|
+
compat: {
|
|
3549
|
+
supportsStore: false,
|
|
3550
|
+
supportsDeveloperRole: false,
|
|
3551
|
+
supportsReasoningEffort: false,
|
|
3552
|
+
},
|
|
3553
|
+
}
|
|
3554
|
+
: {}),
|
|
3555
|
+
}
|
|
3556
|
+
: {
|
|
3557
|
+
...defaults,
|
|
3558
|
+
api,
|
|
3559
|
+
baseUrl,
|
|
3560
|
+
name,
|
|
3561
|
+
input,
|
|
3562
|
+
contextWindow: defaultTierWindow,
|
|
3563
|
+
maxTokens,
|
|
3564
|
+
headers: { ...COPILOT_API_HEADERS },
|
|
3565
|
+
...(api === "openai-completions"
|
|
3566
|
+
? {
|
|
3567
|
+
compat: {
|
|
3568
|
+
supportsStore: false,
|
|
3569
|
+
supportsDeveloperRole: false,
|
|
3570
|
+
supportsReasoningEffort: false,
|
|
3571
|
+
},
|
|
3572
|
+
}
|
|
3573
|
+
: {}),
|
|
3574
|
+
};
|
|
3575
|
+
const variant = createCopilotLongContextVariant(
|
|
3576
|
+
base,
|
|
3577
|
+
contextWindow,
|
|
3578
|
+
maxTokens,
|
|
3579
|
+
tokenPrices.longContext,
|
|
3580
|
+
);
|
|
3581
|
+
if (variant) {
|
|
3582
|
+
longContextVariants.push(variant);
|
|
3583
|
+
// Overflowing the default tier promotes into the 1M sibling
|
|
3584
|
+
// unless the reference already pins a target.
|
|
3585
|
+
base.contextPromotionTarget ??= `github-copilot/${variant.id}`;
|
|
3586
|
+
}
|
|
3587
|
+
return base;
|
|
3588
|
+
},
|
|
3589
|
+
fetch: config?.fetch,
|
|
3590
|
+
});
|
|
3591
|
+
if (models === null) {
|
|
3592
|
+
return null;
|
|
3593
|
+
}
|
|
3594
|
+
// Append synthesized tiers; a real upstream id always wins over a
|
|
3595
|
+
// local variant with the same id.
|
|
3596
|
+
const takenIds = new Set(models.map(model => model.id));
|
|
3597
|
+
for (const variant of longContextVariants) {
|
|
3598
|
+
if (takenIds.has(variant.id)) {
|
|
3599
|
+
continue;
|
|
3600
|
+
}
|
|
3601
|
+
takenIds.add(variant.id);
|
|
3602
|
+
models.push(variant);
|
|
3603
|
+
}
|
|
3604
|
+
return models.sort((left, right) => left.id.localeCompare(right.id));
|
|
3605
|
+
},
|
|
3606
|
+
}),
|
|
3607
|
+
};
|
|
3608
|
+
}
|
|
3609
|
+
|
|
3610
|
+
// ---------------------------------------------------------------------------
|
|
3611
|
+
// 24. Anthropic
|
|
3612
|
+
// ---------------------------------------------------------------------------
|
|
3613
|
+
|
|
3614
|
+
export interface AnthropicModelManagerConfig {
|
|
3615
|
+
apiKey?: string;
|
|
3616
|
+
baseUrl?: string;
|
|
3617
|
+
fetch?: FetchImpl;
|
|
3618
|
+
}
|
|
3619
|
+
|
|
3620
|
+
export function anthropicModelManagerOptions(
|
|
3621
|
+
config?: AnthropicModelManagerConfig,
|
|
3622
|
+
): ModelManagerOptions<"anthropic-messages"> {
|
|
3623
|
+
const apiKey = config?.apiKey;
|
|
3624
|
+
const baseUrl = config?.baseUrl ?? ANTHROPIC_BASE_URL;
|
|
3625
|
+
return {
|
|
3626
|
+
providerId: "anthropic",
|
|
3627
|
+
modelsDev: {
|
|
3628
|
+
fetch: () => fetchModelsDevPayload(config?.fetch),
|
|
3629
|
+
map: payload => mapAnthropicModelsDev(payload, baseUrl),
|
|
3630
|
+
},
|
|
3631
|
+
...(apiKey && {
|
|
3632
|
+
fetchDynamicModels: async () => {
|
|
3633
|
+
const modelsDevModels = await fetchModelsDevPayload(config?.fetch)
|
|
3634
|
+
.then(payload => mapAnthropicModelsDev(payload, baseUrl))
|
|
3635
|
+
.catch(() => []);
|
|
3636
|
+
const references = buildAnthropicReferenceMap(modelsDevModels);
|
|
3637
|
+
return (
|
|
3638
|
+
fetchOpenAICompatibleModels({
|
|
3639
|
+
api: "anthropic-messages",
|
|
3640
|
+
provider: "anthropic",
|
|
3641
|
+
baseUrl,
|
|
3642
|
+
headers: buildAnthropicDiscoveryHeaders(apiKey),
|
|
3643
|
+
mapModel: (
|
|
3644
|
+
entry: OpenAICompatibleModelRecord,
|
|
3645
|
+
defaults: ModelSpec<"anthropic-messages">,
|
|
3646
|
+
_context: OpenAICompatibleModelMapperContext<"anthropic-messages">,
|
|
3647
|
+
): ModelSpec<"anthropic-messages"> => {
|
|
3648
|
+
const discoveredName = typeof entry.display_name === "string" ? entry.display_name : defaults.name;
|
|
3649
|
+
const reference = references.get(defaults.id);
|
|
3650
|
+
if (!reference) {
|
|
3651
|
+
return {
|
|
3652
|
+
...defaults,
|
|
3653
|
+
name: discoveredName,
|
|
3654
|
+
};
|
|
3655
|
+
}
|
|
3656
|
+
return {
|
|
3657
|
+
...reference,
|
|
3658
|
+
id: defaults.id,
|
|
3659
|
+
name: discoveredName,
|
|
3660
|
+
api: "anthropic-messages",
|
|
3661
|
+
provider: "anthropic",
|
|
3662
|
+
baseUrl,
|
|
3663
|
+
};
|
|
3664
|
+
},
|
|
3665
|
+
fetch: config?.fetch,
|
|
3666
|
+
}) ?? null
|
|
3667
|
+
);
|
|
3668
|
+
},
|
|
3669
|
+
}),
|
|
3670
|
+
};
|
|
3671
|
+
}
|
|
3672
|
+
|
|
3673
|
+
// ---------------------------------------------------------------------------
|
|
3674
|
+
// Models.dev provider descriptors for generate-models.ts
|
|
3675
|
+
// ---------------------------------------------------------------------------
|
|
3676
|
+
|
|
3677
|
+
/** Describes how to map models.dev API data for a single provider. */
|
|
3678
|
+
export interface ModelsDevProviderDescriptor {
|
|
3679
|
+
/** Key in the models.dev API response JSON (e.g., "anthropic", "amazon-bedrock") */
|
|
3680
|
+
modelsDevKey: string;
|
|
3681
|
+
/** Provider ID in our system */
|
|
3682
|
+
providerId: string;
|
|
3683
|
+
/** Default API type for this provider's models */
|
|
3684
|
+
api: Api;
|
|
3685
|
+
/** Default base URL */
|
|
3686
|
+
baseUrl: string;
|
|
3687
|
+
/** Default context window fallback (default: UNKNNOWN_CONTEXT_WINDOW) */
|
|
3688
|
+
defaultContextWindow?: number;
|
|
3689
|
+
/** Default max tokens fallback (default: UNKNNOWN_MAX_TOKENS) */
|
|
3690
|
+
defaultMaxTokens?: number;
|
|
3691
|
+
/** Optional compat overrides applied to every model from this provider */
|
|
3692
|
+
compat?: ModelSpec<Api>["compat"];
|
|
3693
|
+
/** Optional static headers applied to every model */
|
|
3694
|
+
headers?: Record<string, string>;
|
|
3695
|
+
/**
|
|
3696
|
+
* Optional filter: return false to skip a model.
|
|
3697
|
+
* Called with (modelId, rawModel). Default: skip if tool_call !== true.
|
|
3698
|
+
*/
|
|
3699
|
+
filterModel?: (modelId: string, model: ModelsDevModel) => boolean;
|
|
3700
|
+
/**
|
|
3701
|
+
* Optional transform: modify the mapped model before it's added.
|
|
3702
|
+
* Can return null to skip the model, or an array to emit multiple models.
|
|
3703
|
+
*/
|
|
3704
|
+
transformModel?: (
|
|
3705
|
+
model: ModelSpec<Api>,
|
|
3706
|
+
modelId: string,
|
|
3707
|
+
raw: ModelsDevModel,
|
|
3708
|
+
) => ModelSpec<Api> | ModelSpec<Api>[] | null;
|
|
3709
|
+
/**
|
|
3710
|
+
* Optional: override the API type per-model.
|
|
3711
|
+
* Called with (modelId, raw). Return the API type to use.
|
|
3712
|
+
* If not provided, uses the `api` field.
|
|
3713
|
+
*/
|
|
3714
|
+
resolveApi?: (modelId: string, raw: ModelsDevModel) => { api: Api; baseUrl: string } | null;
|
|
3715
|
+
}
|
|
3716
|
+
|
|
3717
|
+
/** Generic mapper that converts models.dev data using provider descriptors. */
|
|
3718
|
+
export function mapModelsDevToModels(
|
|
3719
|
+
data: Record<string, unknown>,
|
|
3720
|
+
descriptors: readonly ModelsDevProviderDescriptor[],
|
|
3721
|
+
): ModelSpec<Api>[] {
|
|
3722
|
+
const models: ModelSpec<Api>[] = [];
|
|
3723
|
+
for (const desc of descriptors) {
|
|
3724
|
+
const providerData = (data as Record<string, Record<string, unknown>>)[desc.modelsDevKey];
|
|
3725
|
+
if (!isRecord(providerData) || !isRecord(providerData.models)) continue;
|
|
3726
|
+
|
|
3727
|
+
for (const [modelId, rawModel] of Object.entries(providerData.models)) {
|
|
3728
|
+
if (!isRecord(rawModel)) continue;
|
|
3729
|
+
const m = rawModel as ModelsDevModel;
|
|
3730
|
+
|
|
3731
|
+
// Default filter: tool_call must be true
|
|
3732
|
+
if (desc.filterModel) {
|
|
3733
|
+
if (!desc.filterModel(modelId, m)) continue;
|
|
3734
|
+
} else {
|
|
3735
|
+
if (m.tool_call !== true) continue;
|
|
3736
|
+
}
|
|
3737
|
+
|
|
3738
|
+
// Resolve API and baseUrl (may be per-model for providers like OpenCode)
|
|
3739
|
+
const resolved = desc.resolveApi?.(modelId, m) ?? { api: desc.api, baseUrl: desc.baseUrl };
|
|
3740
|
+
if (!resolved) continue;
|
|
3741
|
+
|
|
3742
|
+
const mapped: ModelSpec<Api> = {
|
|
3743
|
+
id: modelId,
|
|
3744
|
+
name: toModelName(m.name, modelId),
|
|
3745
|
+
api: resolved.api,
|
|
3746
|
+
provider: desc.providerId as ModelSpec<Api>["provider"],
|
|
3747
|
+
baseUrl: resolved.baseUrl,
|
|
3748
|
+
reasoning: m.reasoning === true,
|
|
3749
|
+
input: toInputCapabilities(m.modalities?.input),
|
|
3750
|
+
cost: {
|
|
3751
|
+
input: toNumber(m.cost?.input) ?? 0,
|
|
3752
|
+
output: toNumber(m.cost?.output) ?? 0,
|
|
3753
|
+
cacheRead: toNumber(m.cost?.cache_read) ?? 0,
|
|
3754
|
+
cacheWrite: toNumber(m.cost?.cache_write) ?? 0,
|
|
3755
|
+
},
|
|
3756
|
+
contextWindow: toPositiveNumber(m.limit?.context, desc.defaultContextWindow ?? null),
|
|
3757
|
+
maxTokens: toPositiveNumber(m.limit?.output, desc.defaultMaxTokens ?? null),
|
|
3758
|
+
...(m.tool_call === false ? { supportsTools: false } : {}),
|
|
3759
|
+
...(desc.compat && { compat: desc.compat }),
|
|
3760
|
+
...(desc.headers && { headers: { ...desc.headers } }),
|
|
3761
|
+
};
|
|
3762
|
+
|
|
3763
|
+
// Apply per-model transform
|
|
3764
|
+
if (desc.transformModel) {
|
|
3765
|
+
const result = desc.transformModel(mapped, modelId, m);
|
|
3766
|
+
if (result === null) continue;
|
|
3767
|
+
if (Array.isArray(result)) {
|
|
3768
|
+
models.push(...result);
|
|
3769
|
+
} else {
|
|
3770
|
+
models.push(result);
|
|
3771
|
+
}
|
|
3772
|
+
} else {
|
|
3773
|
+
models.push(mapped);
|
|
3774
|
+
}
|
|
3775
|
+
}
|
|
3776
|
+
}
|
|
3777
|
+
return models;
|
|
3778
|
+
}
|
|
3779
|
+
|
|
3780
|
+
// Bedrock cross-region prefix helpers
|
|
3781
|
+
const BEDROCK_GLOBAL_PREFIXES = [
|
|
3782
|
+
"anthropic.claude-fable-5",
|
|
3783
|
+
"anthropic.claude-mythos-5",
|
|
3784
|
+
"anthropic.claude-haiku-4-5",
|
|
3785
|
+
"anthropic.claude-sonnet-4",
|
|
3786
|
+
"anthropic.claude-opus-4-5",
|
|
3787
|
+
"amazon.nova-2-lite",
|
|
3788
|
+
"cohere.embed-v4",
|
|
3789
|
+
"twelvelabs.pegasus-1-2",
|
|
3790
|
+
];
|
|
3791
|
+
|
|
3792
|
+
const BEDROCK_US_PREFIXES = [
|
|
3793
|
+
"amazon.nova-lite",
|
|
3794
|
+
"amazon.nova-micro",
|
|
3795
|
+
"amazon.nova-premier",
|
|
3796
|
+
"amazon.nova-pro",
|
|
3797
|
+
"anthropic.claude-3-7-sonnet",
|
|
3798
|
+
"anthropic.claude-opus-4-1",
|
|
3799
|
+
"anthropic.claude-opus-4-20250514",
|
|
3800
|
+
"deepseek.r1",
|
|
3801
|
+
"meta.llama3-2",
|
|
3802
|
+
"meta.llama3-3",
|
|
3803
|
+
"meta.llama4",
|
|
3804
|
+
];
|
|
3805
|
+
|
|
3806
|
+
function bedrockCrossRegionId(id: string): string {
|
|
3807
|
+
if (BEDROCK_GLOBAL_PREFIXES.some(p => id.startsWith(p))) return `global.${id}`;
|
|
3808
|
+
if (BEDROCK_US_PREFIXES.some(p => id.startsWith(p))) return `us.${id}`;
|
|
3809
|
+
return id;
|
|
3810
|
+
}
|
|
3811
|
+
|
|
3812
|
+
interface ApiResolutionRule {
|
|
3813
|
+
matches: (modelId: string, raw: ModelsDevModel) => boolean;
|
|
3814
|
+
resolved: { api: Api; baseUrl: string };
|
|
3815
|
+
}
|
|
3816
|
+
|
|
3817
|
+
function resolveApiByRules(
|
|
3818
|
+
modelId: string,
|
|
3819
|
+
raw: ModelsDevModel,
|
|
3820
|
+
rules: readonly ApiResolutionRule[],
|
|
3821
|
+
fallback: { api: Api; baseUrl: string },
|
|
3822
|
+
): { api: Api; baseUrl: string } {
|
|
3823
|
+
for (const rule of rules) {
|
|
3824
|
+
if (rule.matches(modelId, raw)) return rule.resolved;
|
|
3825
|
+
}
|
|
3826
|
+
return fallback;
|
|
3827
|
+
}
|
|
3828
|
+
|
|
3829
|
+
function createOpenCodeApiResolution(
|
|
3830
|
+
basePath: string,
|
|
3831
|
+
idOverrides: Readonly<Record<string, Api>> = {},
|
|
3832
|
+
): {
|
|
3833
|
+
defaultResolution: { api: Api; baseUrl: string };
|
|
3834
|
+
rules: ApiResolutionRule[];
|
|
3835
|
+
} {
|
|
3836
|
+
const completionsBaseUrl = `${basePath}/v1`;
|
|
3837
|
+
// Per-API base URLs on the OpenCode-style endpoint:
|
|
3838
|
+
// - openai-completions / openai-responses / google-generative-ai → /v1
|
|
3839
|
+
// - anthropic-messages → bare basePath (the Anthropic client appends /v1/messages)
|
|
3840
|
+
const baseUrlForApi = (api: Api): string => (api === "anthropic-messages" ? basePath : completionsBaseUrl);
|
|
3841
|
+
const overrideRules: ApiResolutionRule[] = Object.entries(idOverrides).map(([id, api]) => ({
|
|
3842
|
+
matches: modelId => modelId === id,
|
|
3843
|
+
resolved: { api, baseUrl: baseUrlForApi(api) },
|
|
3844
|
+
}));
|
|
3845
|
+
return {
|
|
3846
|
+
defaultResolution: { api: "openai-completions", baseUrl: completionsBaseUrl },
|
|
3847
|
+
rules: [
|
|
3848
|
+
// Per-id overrides take precedence over npm-based heuristics so we can
|
|
3849
|
+
// correct upstream metadata mismatches (see OPENCODE_GO_API_RESOLUTION).
|
|
3850
|
+
...overrideRules,
|
|
3851
|
+
{
|
|
3852
|
+
matches: (_modelId, raw) => raw.provider?.npm === "@ai-sdk/openai",
|
|
3853
|
+
resolved: { api: "openai-responses", baseUrl: completionsBaseUrl },
|
|
3854
|
+
},
|
|
3855
|
+
{
|
|
3856
|
+
matches: (_modelId, raw) => raw.provider?.npm === "@ai-sdk/anthropic",
|
|
3857
|
+
resolved: { api: "anthropic-messages", baseUrl: basePath },
|
|
3858
|
+
},
|
|
3859
|
+
{
|
|
3860
|
+
matches: (_modelId, raw) => raw.provider?.npm === "@ai-sdk/google",
|
|
3861
|
+
resolved: { api: "google-generative-ai", baseUrl: completionsBaseUrl },
|
|
3862
|
+
},
|
|
3863
|
+
],
|
|
3864
|
+
};
|
|
3865
|
+
}
|
|
3866
|
+
|
|
3867
|
+
// OpenCode Zen: models.dev declares minimax-m3-free (and forward-compat
|
|
3868
|
+
// minimax-m3) with `provider.npm = "@ai-sdk/anthropic"`, but the Zen gateway
|
|
3869
|
+
// only serves them at https://opencode.ai/zen/v1/chat/completions (verified
|
|
3870
|
+
// against the live /v1/models response — minimax-m3-free is listed there, and
|
|
3871
|
+
// the gateway has no /v1/messages route for it). Without this override the
|
|
3872
|
+
// resolver POSTs anthropic-shaped requests to /v1/messages and the UI surfaces
|
|
3873
|
+
// raw <invoke>/<|minimax|>/<tool_call> markup (#1617).
|
|
3874
|
+
const OPENCODE_ZEN_API_RESOLUTION = createOpenCodeApiResolution("https://opencode.ai/zen", {
|
|
3875
|
+
"minimax-m3": "openai-completions",
|
|
3876
|
+
"minimax-m3-free": "openai-completions",
|
|
3877
|
+
});
|
|
3878
|
+
// OpenCode Go: models.dev declares minimax-m2.7 / qwen3.5-plus / qwen3.6-plus
|
|
3879
|
+
// (and now also minimax-m3) with `provider.npm = "@ai-sdk/anthropic"`, but
|
|
3880
|
+
// the OpenCode Go gateway only serves them at
|
|
3881
|
+
// `https://opencode.ai/zen/go/v1/chat/completions` (verified against
|
|
3882
|
+
// https://opencode.ai/zen/go/v1/models and the upstream endpoint table at
|
|
3883
|
+
// https://opencode.ai/docs/go/#endpoints — minimax-m2.5 works the same way
|
|
3884
|
+
// and lacks an `npm` field on models.dev so it already falls through to the
|
|
3885
|
+
// openai-completions default). Without this override the resolver would POST
|
|
3886
|
+
// anthropic-style requests to /v1/messages and the gateway would return its
|
|
3887
|
+
// `Page Not Found` HTML (issue #887 for the qwen/m2.7 entries; minimax-m3
|
|
3888
|
+
// and minimax-m3-free added under #1617 for the same root cause).
|
|
3889
|
+
const OPENCODE_GO_API_RESOLUTION = createOpenCodeApiResolution("https://opencode.ai/zen/go", {
|
|
3890
|
+
"minimax-m2.7": "openai-completions",
|
|
3891
|
+
"minimax-m3": "openai-completions",
|
|
3892
|
+
"minimax-m3-free": "openai-completions",
|
|
3893
|
+
"qwen3.5-plus": "openai-completions",
|
|
3894
|
+
"qwen3.6-plus": "openai-completions",
|
|
3895
|
+
});
|
|
3896
|
+
|
|
3897
|
+
const COPILOT_BASE_URL = "https://api.githubcopilot.com";
|
|
3898
|
+
|
|
3899
|
+
const COPILOT_DEFAULT_RESOLUTION = {
|
|
3900
|
+
api: "openai-completions",
|
|
3901
|
+
baseUrl: COPILOT_BASE_URL,
|
|
3902
|
+
} as const satisfies { api: Api; baseUrl: string };
|
|
3903
|
+
|
|
3904
|
+
const COPILOT_API_RESOLUTION_RULES: readonly ApiResolutionRule[] = [
|
|
3905
|
+
{
|
|
3906
|
+
matches: modelId => COPILOT_ANTHROPIC_MODEL_PATTERN.test(modelId),
|
|
3907
|
+
resolved: { api: "anthropic-messages", baseUrl: COPILOT_BASE_URL },
|
|
3908
|
+
},
|
|
3909
|
+
{
|
|
3910
|
+
matches: isCopilotResponsesModelId,
|
|
3911
|
+
resolved: { api: "openai-responses", baseUrl: COPILOT_BASE_URL },
|
|
3912
|
+
},
|
|
3913
|
+
];
|
|
3914
|
+
|
|
3915
|
+
function simpleModelsDevDescriptor(
|
|
3916
|
+
modelsDevKey: string,
|
|
3917
|
+
providerId: string,
|
|
3918
|
+
api: Api,
|
|
3919
|
+
baseUrl: string,
|
|
3920
|
+
options: Omit<ModelsDevProviderDescriptor, "modelsDevKey" | "providerId" | "api" | "baseUrl"> = {},
|
|
3921
|
+
): ModelsDevProviderDescriptor {
|
|
3922
|
+
return {
|
|
3923
|
+
modelsDevKey,
|
|
3924
|
+
providerId,
|
|
3925
|
+
api,
|
|
3926
|
+
baseUrl,
|
|
3927
|
+
...options,
|
|
3928
|
+
};
|
|
3929
|
+
}
|
|
3930
|
+
|
|
3931
|
+
function openAiCompletionsDescriptor(
|
|
3932
|
+
modelsDevKey: string,
|
|
3933
|
+
providerId: string,
|
|
3934
|
+
baseUrl: string,
|
|
3935
|
+
options: Omit<ModelsDevProviderDescriptor, "modelsDevKey" | "providerId" | "api" | "baseUrl"> = {},
|
|
3936
|
+
): ModelsDevProviderDescriptor {
|
|
3937
|
+
return simpleModelsDevDescriptor(modelsDevKey, providerId, "openai-completions", baseUrl, options);
|
|
3938
|
+
}
|
|
3939
|
+
|
|
3940
|
+
function anthropicMessagesDescriptor(
|
|
3941
|
+
modelsDevKey: string,
|
|
3942
|
+
providerId: string,
|
|
3943
|
+
baseUrl: string,
|
|
3944
|
+
options: Omit<ModelsDevProviderDescriptor, "modelsDevKey" | "providerId" | "api" | "baseUrl"> = {},
|
|
3945
|
+
): ModelsDevProviderDescriptor {
|
|
3946
|
+
return simpleModelsDevDescriptor(modelsDevKey, providerId, "anthropic-messages", baseUrl, options);
|
|
3947
|
+
}
|
|
3948
|
+
|
|
3949
|
+
const GOOGLE_VERTEX_BASE_URL = "https://{location}-aiplatform.googleapis.com";
|
|
3950
|
+
const GOOGLE_VERTEX_OPENAI_BASE_URL =
|
|
3951
|
+
"https://{location}-aiplatform.googleapis.com/v1/projects/{project}/locations/{location}/endpoints/openapi";
|
|
3952
|
+
const GOOGLE_VERTEX_ANTHROPIC_BASE_URL =
|
|
3953
|
+
"https://{location}-aiplatform.googleapis.com/v1/projects/{project}/locations/{location}/publishers/anthropic/models/{model}:streamRawPredict";
|
|
3954
|
+
|
|
3955
|
+
function resolveGoogleVertexApi(modelId: string, raw: ModelsDevModel): { api: Api; baseUrl: string } {
|
|
3956
|
+
if (raw.provider?.npm === "@ai-sdk/google-vertex/anthropic") {
|
|
3957
|
+
return {
|
|
3958
|
+
api: "anthropic-messages",
|
|
3959
|
+
baseUrl: GOOGLE_VERTEX_ANTHROPIC_BASE_URL.replace("{model}", modelId),
|
|
3960
|
+
};
|
|
3961
|
+
}
|
|
3962
|
+
if (modelId.includes("/") || raw.provider?.npm === "@ai-sdk/openai-compatible") {
|
|
3963
|
+
return { api: "openai-completions", baseUrl: GOOGLE_VERTEX_OPENAI_BASE_URL };
|
|
3964
|
+
}
|
|
3965
|
+
return { api: "google-vertex", baseUrl: GOOGLE_VERTEX_BASE_URL };
|
|
3966
|
+
}
|
|
3967
|
+
|
|
3968
|
+
const MODELS_DEV_PROVIDER_DESCRIPTORS_BEDROCK: readonly ModelsDevProviderDescriptor[] = [
|
|
3969
|
+
// --- Amazon Bedrock ---
|
|
3970
|
+
{
|
|
3971
|
+
modelsDevKey: "amazon-bedrock",
|
|
3972
|
+
providerId: "amazon-bedrock",
|
|
3973
|
+
api: "bedrock-converse-stream",
|
|
3974
|
+
baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com",
|
|
3975
|
+
filterModel: (id, m) => {
|
|
3976
|
+
if (m.tool_call !== true) return false;
|
|
3977
|
+
if (id.startsWith("ai21.jamba")) return false;
|
|
3978
|
+
if (id.startsWith("amazon.titan-text-express") || id.startsWith("mistral.mistral-7b-instruct-v0"))
|
|
3979
|
+
return false;
|
|
3980
|
+
return true;
|
|
3981
|
+
},
|
|
3982
|
+
transformModel: (model, modelId, m) => {
|
|
3983
|
+
const crossRegionId = bedrockCrossRegionId(modelId);
|
|
3984
|
+
const bedrockModel: ModelSpec<Api> = {
|
|
3985
|
+
...model,
|
|
3986
|
+
id: crossRegionId,
|
|
3987
|
+
name: toModelName(m.name, crossRegionId),
|
|
3988
|
+
};
|
|
3989
|
+
// Also emit EU variants for Claude models
|
|
3990
|
+
if (modelId.startsWith("anthropic.claude-")) {
|
|
3991
|
+
return [
|
|
3992
|
+
bedrockModel,
|
|
3993
|
+
{
|
|
3994
|
+
...bedrockModel,
|
|
3995
|
+
id: `eu.${modelId}`,
|
|
3996
|
+
name: `${toModelName(m.name, modelId)} (EU)`,
|
|
3997
|
+
},
|
|
3998
|
+
];
|
|
3999
|
+
}
|
|
4000
|
+
return bedrockModel;
|
|
4001
|
+
},
|
|
4002
|
+
},
|
|
4003
|
+
];
|
|
4004
|
+
|
|
4005
|
+
const MODELS_DEV_PROVIDER_DESCRIPTORS_CORE: readonly ModelsDevProviderDescriptor[] = [
|
|
4006
|
+
// --- Anthropic ---
|
|
4007
|
+
anthropicMessagesDescriptor("anthropic", "anthropic", "https://api.anthropic.com", {
|
|
4008
|
+
filterModel: (id, m) => {
|
|
4009
|
+
if (m.tool_call !== true) return false;
|
|
4010
|
+
if (
|
|
4011
|
+
id.startsWith("claude-3-5-haiku") ||
|
|
4012
|
+
id.startsWith("claude-3-7-sonnet") ||
|
|
4013
|
+
id === "claude-3-opus-20240229" ||
|
|
4014
|
+
id === "claude-3-sonnet-20240229"
|
|
4015
|
+
)
|
|
4016
|
+
return false;
|
|
4017
|
+
return true;
|
|
4018
|
+
},
|
|
4019
|
+
}),
|
|
4020
|
+
// --- Google ---
|
|
4021
|
+
simpleModelsDevDescriptor(
|
|
4022
|
+
"google",
|
|
4023
|
+
"google",
|
|
4024
|
+
"google-generative-ai",
|
|
4025
|
+
"https://generativelanguage.googleapis.com/v1beta",
|
|
4026
|
+
),
|
|
4027
|
+
// --- OpenAI ---
|
|
4028
|
+
simpleModelsDevDescriptor("openai", "openai", "openai-responses", "https://api.openai.com/v1"),
|
|
4029
|
+
// --- Groq ---
|
|
4030
|
+
openAiCompletionsDescriptor("groq", "groq", "https://api.groq.com/openai/v1"),
|
|
4031
|
+
// --- Cerebras ---
|
|
4032
|
+
openAiCompletionsDescriptor("cerebras", "cerebras", "https://api.cerebras.ai/v1"),
|
|
4033
|
+
// --- Together ---
|
|
4034
|
+
openAiCompletionsDescriptor("togetherai", "together", "https://api.together.xyz/v1"),
|
|
4035
|
+
// --- CoreWeave Serverless Inference ---
|
|
4036
|
+
openAiCompletionsDescriptor("wandb", "coreweave", "https://api.inference.wandb.ai/v1", {
|
|
4037
|
+
transformModel: model => {
|
|
4038
|
+
if (!model.id.startsWith("openai/gpt-oss-")) {
|
|
4039
|
+
return model;
|
|
4040
|
+
}
|
|
4041
|
+
return {
|
|
4042
|
+
...model,
|
|
4043
|
+
reasoning: true,
|
|
4044
|
+
thinking: { mode: "effort", efforts: [Effort.Low, Effort.Medium, Effort.High] },
|
|
4045
|
+
};
|
|
4046
|
+
},
|
|
4047
|
+
}),
|
|
4048
|
+
// --- NVIDIA ---
|
|
4049
|
+
openAiCompletionsDescriptor("nvidia", "nvidia", "https://integrate.api.nvidia.com/v1", {
|
|
4050
|
+
defaultContextWindow: 131072,
|
|
4051
|
+
}),
|
|
4052
|
+
// --- xAI ---
|
|
4053
|
+
openAiCompletionsDescriptor("xai", "xai", "https://api.x.ai/v1"),
|
|
4054
|
+
// --- DeepSeek ---
|
|
4055
|
+
openAiCompletionsDescriptor("deepseek", "deepseek", "https://api.deepseek.com", {
|
|
4056
|
+
// Only ship the v4 family as built-ins; older deepseek-chat / deepseek-reasoner
|
|
4057
|
+
// ids are kept off the catalog until the issue thread asks for them.
|
|
4058
|
+
filterModel: (id, m) => m.tool_call === true && id.startsWith("deepseek-v4"),
|
|
4059
|
+
compat: {
|
|
4060
|
+
// DeepSeek V4 effort remapping is derived in model-thinking metadata; this
|
|
4061
|
+
// descriptor keeps only transport-shape compat.
|
|
4062
|
+
supportsDeveloperRole: false,
|
|
4063
|
+
supportsReasoningEffort: true,
|
|
4064
|
+
maxTokensField: "max_tokens",
|
|
4065
|
+
// DeepSeek V4 thinking mode rejects the `tool_choice` control parameter.
|
|
4066
|
+
// Tool calls still work without it; the API defaults to auto when tools exist.
|
|
4067
|
+
supportsToolChoice: false,
|
|
4068
|
+
// DeepSeek V4's OpenAI format docs enable thinking with both the toggle and
|
|
4069
|
+
// reasoning_effort. Keep the toggle explicit for built-in models.
|
|
4070
|
+
extraBody: { thinking: { type: "enabled" } },
|
|
4071
|
+
// DeepSeek emits chain-of-thought via `reasoning_content` and requires it
|
|
4072
|
+
// to round-trip on assistant tool-call messages so the model can resume
|
|
4073
|
+
// from prior thinking (interleaved.field=reasoning_content on models.dev,
|
|
4074
|
+
// matches the kimi/openrouter handling already in detectCompat).
|
|
4075
|
+
reasoningContentField: "reasoning_content",
|
|
4076
|
+
requiresReasoningContentForToolCalls: true,
|
|
4077
|
+
requiresAssistantContentForToolCalls: true,
|
|
4078
|
+
},
|
|
4079
|
+
}),
|
|
4080
|
+
];
|
|
4081
|
+
|
|
4082
|
+
const MODELS_DEV_PROVIDER_DESCRIPTORS_CODING_PLANS: readonly ModelsDevProviderDescriptor[] = [
|
|
4083
|
+
// --- zAI ---
|
|
4084
|
+
anthropicMessagesDescriptor("zai-coding-plan", "zai", "https://api.z.ai/api/anthropic"),
|
|
4085
|
+
// --- Umans AI Coding Plan ---
|
|
4086
|
+
anthropicMessagesDescriptor("umans-ai-coding-plan", "umans", UMANS_BASE_URL),
|
|
4087
|
+
// --- Xiaomi ---
|
|
4088
|
+
openAiCompletionsDescriptor("xiaomi", "xiaomi", "https://api.xiaomimimo.com/v1", {
|
|
4089
|
+
defaultContextWindow: 262144,
|
|
4090
|
+
defaultMaxTokens: 8192,
|
|
4091
|
+
compat: {
|
|
4092
|
+
supportsStore: false,
|
|
4093
|
+
thinkingFormat: "zai",
|
|
4094
|
+
reasoningContentField: "reasoning_content",
|
|
4095
|
+
requiresReasoningContentForToolCalls: true,
|
|
4096
|
+
allowsSyntheticReasoningContentForToolCalls: false,
|
|
4097
|
+
},
|
|
4098
|
+
}),
|
|
4099
|
+
// --- MiniMax Coding Plan ---
|
|
4100
|
+
openAiCompletionsDescriptor("minimax-coding-plan", "minimax-code", "https://api.minimax.io/v1", {
|
|
4101
|
+
compat: {
|
|
4102
|
+
supportsStore: false,
|
|
4103
|
+
supportsDeveloperRole: false,
|
|
4104
|
+
supportsReasoningEffort: false,
|
|
4105
|
+
reasoningContentField: "reasoning_content",
|
|
4106
|
+
},
|
|
4107
|
+
}),
|
|
4108
|
+
openAiCompletionsDescriptor("minimax-cn-coding-plan", "minimax-code-cn", "https://api.minimaxi.com/v1", {
|
|
4109
|
+
compat: {
|
|
4110
|
+
supportsStore: false,
|
|
4111
|
+
supportsDeveloperRole: false,
|
|
4112
|
+
supportsReasoningEffort: false,
|
|
4113
|
+
reasoningContentField: "reasoning_content",
|
|
4114
|
+
},
|
|
4115
|
+
}),
|
|
4116
|
+
// --- Alibaba Coding Plan ---
|
|
4117
|
+
openAiCompletionsDescriptor(
|
|
4118
|
+
"alibaba-coding-plan",
|
|
4119
|
+
"alibaba-coding-plan",
|
|
4120
|
+
"https://coding-intl.dashscope.aliyuncs.com/v1",
|
|
4121
|
+
{
|
|
4122
|
+
compat: {
|
|
4123
|
+
supportsDeveloperRole: false,
|
|
4124
|
+
},
|
|
4125
|
+
},
|
|
4126
|
+
),
|
|
4127
|
+
// --- Zhipu Coding Plan ---
|
|
4128
|
+
openAiCompletionsDescriptor(
|
|
4129
|
+
"zhipuai-coding-plan",
|
|
4130
|
+
"zhipu-coding-plan",
|
|
4131
|
+
"https://open.bigmodel.cn/api/coding/paas/v4",
|
|
4132
|
+
{
|
|
4133
|
+
compat: {
|
|
4134
|
+
thinkingFormat: "zai",
|
|
4135
|
+
reasoningContentField: "reasoning_content",
|
|
4136
|
+
supportsDeveloperRole: false,
|
|
4137
|
+
},
|
|
4138
|
+
},
|
|
4139
|
+
),
|
|
4140
|
+
];
|
|
4141
|
+
|
|
4142
|
+
const filterActiveToolCallModels = (_id: string, m: ModelsDevModel): boolean => {
|
|
4143
|
+
if (m.tool_call !== true) return false;
|
|
4144
|
+
if (m.status === "deprecated") return false;
|
|
4145
|
+
return true;
|
|
4146
|
+
};
|
|
4147
|
+
|
|
4148
|
+
const MODELS_DEV_PROVIDER_DESCRIPTORS_GOOGLE_VERTEX: readonly ModelsDevProviderDescriptor[] = [
|
|
4149
|
+
simpleModelsDevDescriptor("google-vertex", "google-vertex", "google-vertex", GOOGLE_VERTEX_BASE_URL, {
|
|
4150
|
+
filterModel: filterActiveToolCallModels,
|
|
4151
|
+
resolveApi: resolveGoogleVertexApi,
|
|
4152
|
+
}),
|
|
4153
|
+
];
|
|
4154
|
+
|
|
4155
|
+
const MODELS_DEV_PROVIDER_DESCRIPTORS_SPECIALIZED: readonly ModelsDevProviderDescriptor[] = [
|
|
4156
|
+
// --- Azure OpenAI ---
|
|
4157
|
+
// OpenAI-family models hosted on Azure, served via the Responses API. baseUrl
|
|
4158
|
+
// is empty: the deployment host is per-resource and resolved at runtime from
|
|
4159
|
+
// AZURE_OPENAI_BASE_URL / AZURE_OPENAI_RESOURCE_NAME (see resolveAzureConfig).
|
|
4160
|
+
simpleModelsDevDescriptor("azure", "azure", "azure-openai-responses", "", {
|
|
4161
|
+
filterModel: (modelId, m) => {
|
|
4162
|
+
if (m.tool_call !== true) return false;
|
|
4163
|
+
// OpenAI-family only (not Foundry/DeepSeek/Claude/Llama/Mistral/Phi, which
|
|
4164
|
+
// Azure serves via non-Responses APIs under a per-model provider override).
|
|
4165
|
+
return /^(gpt-|o1|o3|o4|codex|chatgpt)/.test(modelId);
|
|
4166
|
+
},
|
|
4167
|
+
}),
|
|
4168
|
+
// --- Cloudflare AI Gateway ---
|
|
4169
|
+
anthropicMessagesDescriptor(
|
|
4170
|
+
"cloudflare-ai-gateway",
|
|
4171
|
+
"cloudflare-ai-gateway",
|
|
4172
|
+
"https://gateway.ai.cloudflare.com/v1/<account>/<gateway>/anthropic",
|
|
4173
|
+
),
|
|
4174
|
+
// --- Mistral ---
|
|
4175
|
+
openAiCompletionsDescriptor("mistral", "mistral", "https://api.mistral.ai/v1"),
|
|
4176
|
+
// --- OpenCode Zen ---
|
|
4177
|
+
openAiCompletionsDescriptor("opencode", "opencode-zen", "https://opencode.ai/zen/v1", {
|
|
4178
|
+
filterModel: filterActiveToolCallModels,
|
|
4179
|
+
resolveApi: (modelId, raw) =>
|
|
4180
|
+
resolveApiByRules(
|
|
4181
|
+
modelId,
|
|
4182
|
+
raw,
|
|
4183
|
+
OPENCODE_ZEN_API_RESOLUTION.rules,
|
|
4184
|
+
OPENCODE_ZEN_API_RESOLUTION.defaultResolution,
|
|
4185
|
+
),
|
|
4186
|
+
}),
|
|
4187
|
+
// --- OpenCode Go ---
|
|
4188
|
+
openAiCompletionsDescriptor("opencode-go", "opencode-go", "https://opencode.ai/zen/go/v1", {
|
|
4189
|
+
filterModel: filterActiveToolCallModels,
|
|
4190
|
+
resolveApi: (modelId, raw) =>
|
|
4191
|
+
resolveApiByRules(
|
|
4192
|
+
modelId,
|
|
4193
|
+
raw,
|
|
4194
|
+
OPENCODE_GO_API_RESOLUTION.rules,
|
|
4195
|
+
OPENCODE_GO_API_RESOLUTION.defaultResolution,
|
|
4196
|
+
),
|
|
4197
|
+
}),
|
|
4198
|
+
// --- GitHub Copilot ---
|
|
4199
|
+
openAiCompletionsDescriptor("github-copilot", "github-copilot", COPILOT_BASE_URL, {
|
|
4200
|
+
defaultContextWindow: 128000,
|
|
4201
|
+
defaultMaxTokens: 8192,
|
|
4202
|
+
headers: { ...COPILOT_API_HEADERS },
|
|
4203
|
+
filterModel: filterActiveToolCallModels,
|
|
4204
|
+
resolveApi: (modelId, raw) =>
|
|
4205
|
+
resolveApiByRules(modelId, raw, COPILOT_API_RESOLUTION_RULES, COPILOT_DEFAULT_RESOLUTION),
|
|
4206
|
+
transformModel: model => {
|
|
4207
|
+
// compat only applies to openai-completions models
|
|
4208
|
+
if (model.api === "openai-completions") {
|
|
4209
|
+
return {
|
|
4210
|
+
...model,
|
|
4211
|
+
compat: {
|
|
4212
|
+
supportsStore: false,
|
|
4213
|
+
supportsDeveloperRole: false,
|
|
4214
|
+
supportsReasoningEffort: false,
|
|
4215
|
+
},
|
|
4216
|
+
};
|
|
4217
|
+
}
|
|
4218
|
+
return model;
|
|
4219
|
+
},
|
|
4220
|
+
}),
|
|
4221
|
+
// --- MiniMax (Anthropic) ---
|
|
4222
|
+
anthropicMessagesDescriptor("minimax", "minimax", "https://api.minimax.io/anthropic"),
|
|
4223
|
+
anthropicMessagesDescriptor("minimax-cn", "minimax-cn", "https://api.minimaxi.com/anthropic"),
|
|
4224
|
+
// --- Hugging Face ---
|
|
4225
|
+
openAiCompletionsDescriptor("huggingface", "huggingface", "https://router.huggingface.co/v1"),
|
|
4226
|
+
// --- Kilo Gateway ---
|
|
4227
|
+
openAiCompletionsDescriptor("kilo", "kilo", "https://api.kilo.ai/api/gateway"),
|
|
4228
|
+
// --- Moonshot AI ---
|
|
4229
|
+
openAiCompletionsDescriptor("moonshotai", "moonshot", "https://api.moonshot.ai/v1"),
|
|
4230
|
+
// --- NanoGPT ---
|
|
4231
|
+
openAiCompletionsDescriptor("nano-gpt", "nanogpt", "https://nano-gpt.com/api/v1"),
|
|
4232
|
+
// --- Synthetic ---
|
|
4233
|
+
openAiCompletionsDescriptor("synthetic", "synthetic", "https://api.synthetic.new/openai/v1"),
|
|
4234
|
+
// --- Venice AI ---
|
|
4235
|
+
openAiCompletionsDescriptor("venice", "venice", "https://api.venice.ai/api/v1", {
|
|
4236
|
+
transformModel: model => {
|
|
4237
|
+
const maxTokens = clampKimiK27CodeMaxTokens(model.id, model.maxTokens);
|
|
4238
|
+
return maxTokens === model.maxTokens ? model : { ...model, maxTokens };
|
|
4239
|
+
},
|
|
4240
|
+
}),
|
|
4241
|
+
// --- Ollama Cloud ---
|
|
4242
|
+
simpleModelsDevDescriptor("ollama-cloud", "ollama-cloud", "ollama-chat", "https://ollama.com"),
|
|
4243
|
+
// --- Xiaomi Token Plan ---
|
|
4244
|
+
openAiCompletionsDescriptor(
|
|
4245
|
+
"xiaomi-token-plan-ams",
|
|
4246
|
+
"xiaomi-token-plan-ams",
|
|
4247
|
+
"https://token-plan-ams.xiaomimimo.com/v1",
|
|
4248
|
+
),
|
|
4249
|
+
openAiCompletionsDescriptor(
|
|
4250
|
+
"xiaomi-token-plan-cn",
|
|
4251
|
+
"xiaomi-token-plan-cn",
|
|
4252
|
+
"https://token-plan-cn.xiaomimimo.com/v1",
|
|
4253
|
+
),
|
|
4254
|
+
openAiCompletionsDescriptor(
|
|
4255
|
+
"xiaomi-token-plan-sgp",
|
|
4256
|
+
"xiaomi-token-plan-sgp",
|
|
4257
|
+
"https://token-plan-sgp.xiaomimimo.com/v1",
|
|
4258
|
+
),
|
|
4259
|
+
// --- Qwen Portal ---
|
|
4260
|
+
openAiCompletionsDescriptor("qwen-portal", "qwen-portal", "https://portal.qwen.ai/v1", {
|
|
4261
|
+
defaultContextWindow: 128000,
|
|
4262
|
+
defaultMaxTokens: 8192,
|
|
4263
|
+
}),
|
|
4264
|
+
|
|
4265
|
+
// --- ZenMux ---
|
|
4266
|
+
openAiCompletionsDescriptor("zenmux", "zenmux", ZENMUX_OPENAI_BASE_URL, {
|
|
4267
|
+
filterModel: filterActiveToolCallModels,
|
|
4268
|
+
resolveApi: modelId => {
|
|
4269
|
+
if (modelId.startsWith("anthropic/")) {
|
|
4270
|
+
return { api: "anthropic-messages" as const, baseUrl: ZENMUX_ANTHROPIC_BASE_URL };
|
|
4271
|
+
}
|
|
4272
|
+
return { api: "openai-completions" as const, baseUrl: ZENMUX_OPENAI_BASE_URL };
|
|
4273
|
+
},
|
|
4274
|
+
}),
|
|
4275
|
+
];
|
|
4276
|
+
/** All provider descriptors for models.dev data mapping in generate-models.ts. */
|
|
4277
|
+
export const MODELS_DEV_PROVIDER_DESCRIPTORS: readonly ModelsDevProviderDescriptor[] = [
|
|
4278
|
+
...MODELS_DEV_PROVIDER_DESCRIPTORS_BEDROCK,
|
|
4279
|
+
...MODELS_DEV_PROVIDER_DESCRIPTORS_GOOGLE_VERTEX,
|
|
4280
|
+
...MODELS_DEV_PROVIDER_DESCRIPTORS_CORE,
|
|
4281
|
+
...MODELS_DEV_PROVIDER_DESCRIPTORS_CODING_PLANS,
|
|
4282
|
+
...MODELS_DEV_PROVIDER_DESCRIPTORS_SPECIALIZED,
|
|
4283
|
+
];
|