@oh-my-pi/pi-catalog 16.0.4 → 16.0.6
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 +44 -0
- package/dist/types/compat/openai.d.ts +4 -1
- package/dist/types/discovery/antigravity.d.ts +9 -0
- package/dist/types/identity/dialect.d.ts +1 -1
- package/dist/types/identity/family.d.ts +8 -0
- package/dist/types/provider-models/descriptors.d.ts +1 -1
- package/dist/types/provider-models/openai-compat.d.ts +5 -6
- package/dist/types/types.d.ts +109 -13
- package/dist/types/variant-collapse.d.ts +4 -5
- package/dist/types/wire/gemini-headers.d.ts +16 -1
- package/dist/types/wire/github-copilot.d.ts +2 -0
- package/package.json +4 -3
- package/src/build.ts +3 -1
- package/src/compat/openai.ts +213 -19
- package/src/discovery/antigravity.ts +91 -98
- package/src/discovery/codex.ts +33 -40
- package/src/discovery/cursor.ts +31 -24
- package/src/discovery/gemini.ts +39 -30
- package/src/discovery/openai-compatible.ts +22 -32
- package/src/identity/dialect.ts +4 -1
- package/src/identity/family.ts +21 -1
- package/src/model-cache.ts +8 -6
- package/src/model-thinking.ts +24 -6
- package/src/models.json +544 -376
- package/src/provider-models/google.ts +2 -0
- package/src/provider-models/ollama.ts +11 -2
- package/src/provider-models/openai-compat.ts +47 -46
- package/src/types.ts +190 -43
- package/src/variant-collapse.ts +198 -72
- package/src/wire/gemini-headers.ts +28 -5
- package/src/wire/github-copilot.ts +18 -0
package/src/compat/openai.ts
CHANGED
|
@@ -13,12 +13,22 @@ import {
|
|
|
13
13
|
isClaudeModelId,
|
|
14
14
|
isDeepseekModelIdOrName,
|
|
15
15
|
isGlm52ReasoningEffortModelId,
|
|
16
|
+
isGrokReasoningEffortCapable,
|
|
16
17
|
isKimiK26ModelId,
|
|
17
18
|
isKimiModelId,
|
|
18
19
|
isMimoModelIdOrName,
|
|
19
20
|
isQwenModelId,
|
|
21
|
+
modelFamilyToken,
|
|
20
22
|
} from "../identity/family";
|
|
21
|
-
import type {
|
|
23
|
+
import type {
|
|
24
|
+
ModelSpec,
|
|
25
|
+
OpenAICompat,
|
|
26
|
+
OpenAIStreamMarkupHealingPattern,
|
|
27
|
+
ResolvedOpenAICompat,
|
|
28
|
+
ResolvedOpenAIResponsesCompat,
|
|
29
|
+
ResolvedOpenAISharedCompat,
|
|
30
|
+
ResolvedOpenRouterCompat,
|
|
31
|
+
} from "../types";
|
|
22
32
|
import { applyCompatOverrides } from "./apply";
|
|
23
33
|
|
|
24
34
|
/** GLM coding-plan SKUs idle for minutes mid-reasoning; see `streamIdleTimeoutMs`. */
|
|
@@ -28,6 +38,76 @@ const GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;
|
|
|
28
38
|
const DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;
|
|
29
39
|
/** Kimi K2.6 can spend several minutes reasoning before the first visible token. */
|
|
30
40
|
const KIMI_K26_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;
|
|
41
|
+
const MINIMAX_PROVIDER_OR_ID_PATTERN = /minimax/i;
|
|
42
|
+
const DSML_HEALING_PROVIDERS = new Set([
|
|
43
|
+
"ollama",
|
|
44
|
+
"ollama-cloud",
|
|
45
|
+
"nvidia",
|
|
46
|
+
"deepseek",
|
|
47
|
+
"fireworks",
|
|
48
|
+
"nanogpt",
|
|
49
|
+
"opencode-go",
|
|
50
|
+
"openrouter",
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Ollama's OpenAI-compatible `reasoning.effort` only accepts
|
|
55
|
+
* `high|medium|low|max|none`; OMP's `minimal`/`xhigh` levels make the server
|
|
56
|
+
* reject the turn with HTTP 400 `invalid reasoning value`. Map the two
|
|
57
|
+
* unsupported levels onto the closest accepted ones. Stamped in the compat
|
|
58
|
+
* builder (not only at discovery) so stale-cached and custom `ollama`-provider
|
|
59
|
+
* specs are backfilled on every `buildModel`, not just on a fresh
|
|
60
|
+
* `omp models refresh`. Custom OpenAI-compatible providers pointed at a local
|
|
61
|
+
* Ollama port under a different provider id are not covered — they must set
|
|
62
|
+
* `compat.reasoningEffortMap` themselves.
|
|
63
|
+
*/
|
|
64
|
+
const OLLAMA_REASONING_EFFORT_MAP: ResolvedOpenAISharedCompat["reasoningEffortMap"] = { minimal: "low", xhigh: "max" };
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Merge the Ollama default effort map under any explicit overrides (overrides
|
|
68
|
+
* win). No-op off the local `ollama` provider or for non-reasoning models.
|
|
69
|
+
*/
|
|
70
|
+
function mergeOllamaReasoningEffortMap(
|
|
71
|
+
compat: ResolvedOpenAISharedCompat,
|
|
72
|
+
provider: string,
|
|
73
|
+
reasoning: boolean | undefined,
|
|
74
|
+
): void {
|
|
75
|
+
if (provider !== "ollama" || !reasoning) return;
|
|
76
|
+
compat.reasoningEffortMap = { ...OLLAMA_REASONING_EFFORT_MAP, ...compat.reasoningEffortMap };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function resolveReasoningDisableMode(
|
|
80
|
+
thinkingFormat: ResolvedOpenAISharedCompat["thinkingFormat"],
|
|
81
|
+
): ResolvedOpenAISharedCompat["reasoningDisableMode"] {
|
|
82
|
+
switch (thinkingFormat) {
|
|
83
|
+
case "openrouter":
|
|
84
|
+
return "openrouter-enabled-false";
|
|
85
|
+
case "zai":
|
|
86
|
+
return "zai-thinking-disabled";
|
|
87
|
+
case "qwen":
|
|
88
|
+
return "qwen-enable-thinking-false";
|
|
89
|
+
case "qwen-chat-template":
|
|
90
|
+
return "qwen-template-false";
|
|
91
|
+
default:
|
|
92
|
+
return "lowest-effort";
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function detectStreamMarkupHealingPattern(
|
|
97
|
+
provider: string,
|
|
98
|
+
modelId: string,
|
|
99
|
+
): OpenAIStreamMarkupHealingPattern | undefined {
|
|
100
|
+
if (MINIMAX_PROVIDER_OR_ID_PATTERN.test(provider) || MINIMAX_PROVIDER_OR_ID_PATTERN.test(modelId)) {
|
|
101
|
+
return "thinking";
|
|
102
|
+
}
|
|
103
|
+
if (provider === "kimi-code" || provider === "moonshot" || /kimi[-/_.]?k2/i.test(modelId)) {
|
|
104
|
+
return "kimi";
|
|
105
|
+
}
|
|
106
|
+
if (isDeepseekModelIdOrName(modelId) && DSML_HEALING_PROVIDERS.has(provider)) {
|
|
107
|
+
return "dsml";
|
|
108
|
+
}
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
31
111
|
|
|
32
112
|
/**
|
|
33
113
|
* OpenCode's gateways (https://opencode.ai/zen|go) gate `reasoning_content`
|
|
@@ -196,6 +276,25 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
|
|
|
196
276
|
? DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS
|
|
197
277
|
: undefined;
|
|
198
278
|
|
|
279
|
+
const wireModelIdMode: ResolvedOpenAISharedCompat["wireModelIdMode"] =
|
|
280
|
+
provider === "firepass"
|
|
281
|
+
? "firepass"
|
|
282
|
+
: provider === "fireworks"
|
|
283
|
+
? "fireworks"
|
|
284
|
+
: isOpenRouter
|
|
285
|
+
? "openrouter"
|
|
286
|
+
: "raw";
|
|
287
|
+
const thinkingFormat: ResolvedOpenAISharedCompat["thinkingFormat"] =
|
|
288
|
+
isZai || isZhipu || isMoonshotKimi || isXiaomiMimo
|
|
289
|
+
? "zai"
|
|
290
|
+
: isOpenRouter
|
|
291
|
+
? "openrouter"
|
|
292
|
+
: isQwen && isNvidiaNim
|
|
293
|
+
? "qwen-chat-template"
|
|
294
|
+
: isAlibaba || isQwen
|
|
295
|
+
? "qwen"
|
|
296
|
+
: "openai";
|
|
297
|
+
|
|
199
298
|
const compat: ResolvedOpenAICompat = {
|
|
200
299
|
supportsStore: !isNonStandard,
|
|
201
300
|
// `developer` is an OpenAI-Responses-era extension to the chat-completions schema. Almost
|
|
@@ -211,6 +310,10 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
|
|
|
211
310
|
supportsReasoningParams: provider !== "github-copilot",
|
|
212
311
|
reasoningEffortMap: {},
|
|
213
312
|
supportsUsageInStreaming: !isCerebras,
|
|
313
|
+
// pi-ai's thinking-loop guard is gemini-only; default the flag from the
|
|
314
|
+
// family classifier so OpenAI-compat proxies serving Gemini are covered.
|
|
315
|
+
// An opaque alias can opt in via `compat.enableGeminiThinkingLoopGuard`.
|
|
316
|
+
enableGeminiThinkingLoopGuard: modelFamilyToken(spec.id) === "gemini",
|
|
214
317
|
// Kimi (including via OpenRouter and Fireworks router-form IDs such as
|
|
215
318
|
// `accounts/fireworks/routers/kimi-*`) calculates TPM rate limits based on
|
|
216
319
|
// max_tokens, not actual output. The official Kimi K2 model guidance
|
|
@@ -224,7 +327,7 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
|
|
|
224
327
|
supportsForcedToolChoice: true,
|
|
225
328
|
maxTokensField: useMaxTokens ? "max_tokens" : "max_completion_tokens",
|
|
226
329
|
requiresToolResultName: isMistral,
|
|
227
|
-
requiresAssistantAfterToolResult:
|
|
330
|
+
requiresAssistantAfterToolResult: isMistral,
|
|
228
331
|
requiresThinkingAsText: isMistral,
|
|
229
332
|
requiresMistralToolIds: isMistral,
|
|
230
333
|
// Only Kimi's native hosts (Moonshot / Kimi-code, matched by `isMoonshotKimi`)
|
|
@@ -236,16 +339,11 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
|
|
|
236
339
|
// (`chat_template_kwargs.enable_thinking`); top-level `enable_thinking`
|
|
237
340
|
// is rejected by NIM's `additionalProperties: false` request schema
|
|
238
341
|
// (issue #2299).
|
|
239
|
-
thinkingFormat
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
: isQwen && isNvidiaNim
|
|
245
|
-
? "qwen-chat-template"
|
|
246
|
-
: isAlibaba || isQwen
|
|
247
|
-
? "qwen"
|
|
248
|
-
: "openai",
|
|
342
|
+
thinkingFormat,
|
|
343
|
+
reasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),
|
|
344
|
+
omitReasoningEffort: false,
|
|
345
|
+
includeEncryptedReasoning: true,
|
|
346
|
+
filterReasoningHistory: false,
|
|
249
347
|
thinkingKeep: usesMoonshotKimiPreservedThinking ? "all" : undefined,
|
|
250
348
|
reasoningContentField: "reasoning_content",
|
|
251
349
|
// Backends that 400 follow-up requests when prior assistant tool-call turns lack `reasoning_content`:
|
|
@@ -266,6 +364,8 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
|
|
|
266
364
|
(isDeepseekFamily && Boolean(spec.reasoning)) ||
|
|
267
365
|
isXiaomiMimo ||
|
|
268
366
|
(isOpenRouter && Boolean(spec.reasoning)),
|
|
367
|
+
requiresReasoningContentForAllAssistantTurns:
|
|
368
|
+
((isDeepseekFamily && Boolean(spec.reasoning)) || isXiaomiMimo) && !isOpenRouter,
|
|
269
369
|
// DeepSeek V4 and Xiaomi MiMo reject synthetic reasoning_content placeholders (".") on tool-call turns.
|
|
270
370
|
// Kimi and OpenRouter accept them when actual reasoning is unavailable.
|
|
271
371
|
allowsSyntheticReasoningContentForToolCalls: (!isDeepseekFamily || !spec.reasoning) && !isXiaomiMimo,
|
|
@@ -274,20 +374,45 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
|
|
|
274
374
|
openRouterRouting: undefined,
|
|
275
375
|
vercelGatewayRouting: undefined,
|
|
276
376
|
isOpenRouterHost: isOpenRouter,
|
|
377
|
+
wireModelIdMode,
|
|
277
378
|
isVercelGatewayHost: isVercelGateway,
|
|
278
379
|
supportsStrictMode: detectStrictModeSupport(provider, baseUrl),
|
|
279
380
|
extraBody: isDirectDeepseekReasoning ? { thinking: { type: "enabled" } } : undefined,
|
|
280
381
|
toolStrictMode: isCerebras ? "all_strict" : "mixed",
|
|
382
|
+
toolSchemaFlavor: isMoonshotNative ? "moonshot-mfjs" : undefined,
|
|
281
383
|
streamIdleTimeoutMs,
|
|
384
|
+
stripDeepseekSpecialTokens:
|
|
385
|
+
isDeepseekModelIdOrName(spec.id) && (provider === "nvidia" || provider === "deepseek"),
|
|
386
|
+
streamMarkupHealingPattern: detectStreamMarkupHealingPattern(provider, spec.id),
|
|
387
|
+
reasoningDeltasMayBeCumulative:
|
|
388
|
+
MINIMAX_PROVIDER_OR_ID_PATTERN.test(provider) || MINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.id),
|
|
389
|
+
emptyLengthFinishIsContextError: provider === "ollama",
|
|
390
|
+
usesOpenAIToolCallIdLimit: provider === "openai",
|
|
391
|
+
promptCacheSessionHeader: undefined,
|
|
392
|
+
dropThinkingWhenReasoningEffort: provider === "fireworks",
|
|
282
393
|
};
|
|
283
394
|
|
|
284
395
|
applyCompatOverrides(compat, spec.compat);
|
|
396
|
+
if (spec.compat?.reasoningDisableMode === undefined) {
|
|
397
|
+
compat.reasoningDisableMode = resolveReasoningDisableMode(compat.thinkingFormat);
|
|
398
|
+
}
|
|
399
|
+
if (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {
|
|
400
|
+
compat.omitReasoningEffort = true;
|
|
401
|
+
}
|
|
402
|
+
mergeOllamaReasoningEffortMap(compat, provider, spec.reasoning);
|
|
285
403
|
|
|
286
404
|
const whenThinkingPolicy =
|
|
287
405
|
spec.compat?.whenThinking ?? (isOpenCodeProvider && spec.reasoning ? OPENCODE_WHEN_THINKING : undefined);
|
|
288
406
|
if (whenThinkingPolicy) {
|
|
289
407
|
const variant: ResolvedOpenAICompat = { ...compat };
|
|
290
408
|
applyCompatOverrides(variant, whenThinkingPolicy);
|
|
409
|
+
if (whenThinkingPolicy.reasoningDisableMode === undefined) {
|
|
410
|
+
variant.reasoningDisableMode = resolveReasoningDisableMode(variant.thinkingFormat);
|
|
411
|
+
}
|
|
412
|
+
if (whenThinkingPolicy.omitReasoningEffort === undefined && !variant.supportsReasoningEffort) {
|
|
413
|
+
variant.omitReasoningEffort = true;
|
|
414
|
+
}
|
|
415
|
+
mergeOllamaReasoningEffortMap(variant, provider, spec.reasoning);
|
|
291
416
|
compat.whenThinking = variant;
|
|
292
417
|
}
|
|
293
418
|
|
|
@@ -295,9 +420,11 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
|
|
|
295
420
|
}
|
|
296
421
|
|
|
297
422
|
interface OpenAIResponsesSpecLike {
|
|
423
|
+
id?: string;
|
|
298
424
|
provider: string;
|
|
299
425
|
name: string;
|
|
300
426
|
baseUrl: string;
|
|
427
|
+
reasoning?: boolean;
|
|
301
428
|
compat?: OpenAICompat;
|
|
302
429
|
}
|
|
303
430
|
|
|
@@ -315,21 +442,88 @@ interface OpenAIResponsesSpecLike {
|
|
|
315
442
|
export function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): ResolvedOpenAIResponsesCompat {
|
|
316
443
|
const baseUrl = spec.baseUrl ?? "";
|
|
317
444
|
const isAzure = modelMatchesHost({ provider: spec.provider, baseUrl }, "azureOpenAI");
|
|
445
|
+
const isOpenRouter = modelMatchesHost({ provider: spec.provider, baseUrl }, "openrouter");
|
|
446
|
+
const isOpenAIUrl = hostMatchesUrl(baseUrl, "openai");
|
|
447
|
+
const id = spec.id ?? "";
|
|
448
|
+
const thinkingFormat: ResolvedOpenAISharedCompat["thinkingFormat"] = isOpenRouter ? "openrouter" : "openai";
|
|
449
|
+
const isKimiModel = id ? isKimiModelId(id) : false;
|
|
450
|
+
const isDeepseekFamily = id ? isDeepseekModelIdOrName(id) || isDeepseekModelIdOrName(spec.name) : false;
|
|
451
|
+
const reasoningCapable = Boolean(spec.reasoning);
|
|
452
|
+
|
|
318
453
|
const compat: ResolvedOpenAIResponsesCompat = {
|
|
319
|
-
supportsDeveloperRole: isAzure ||
|
|
454
|
+
supportsDeveloperRole: isAzure || isOpenAIUrl || hostMatchesUrl(baseUrl, "githubCopilot"),
|
|
320
455
|
supportsStrictMode:
|
|
321
|
-
spec.provider === "openai" ||
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
hostMatchesUrl(baseUrl, "openai"),
|
|
325
|
-
supportsReasoningEffort: true,
|
|
326
|
-
supportsLongPromptCacheRetention: hostMatchesUrl(baseUrl, "openai"),
|
|
456
|
+
spec.provider === "openai" || isAzure || spec.provider === "github-copilot" || isOpenRouter || isOpenAIUrl,
|
|
457
|
+
supportsReasoningEffort: spec.provider !== "xai-oauth" || isGrokReasoningEffortCapable(id),
|
|
458
|
+
supportsLongPromptCacheRetention: isOpenAIUrl,
|
|
327
459
|
// Azure OpenAI and GitHub Copilot Responses paths require tool results
|
|
328
460
|
// to strictly match prior tool calls when building Responses inputs.
|
|
329
461
|
strictResponsesPairing: isAzure || spec.provider === "github-copilot",
|
|
330
462
|
requiresJuiceZeroHack: spec.name.toLowerCase().startsWith("gpt-5"),
|
|
331
463
|
reasoningEffortMap: {},
|
|
464
|
+
supportsReasoningParams: true,
|
|
465
|
+
thinkingFormat,
|
|
466
|
+
reasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),
|
|
467
|
+
omitReasoningEffort: false,
|
|
468
|
+
includeEncryptedReasoning: spec.provider !== "xai-oauth",
|
|
469
|
+
filterReasoningHistory: spec.provider === "xai-oauth",
|
|
470
|
+
disableReasoningOnForcedToolChoice: isKimiModel,
|
|
471
|
+
disableReasoningOnToolChoice: isDeepseekFamily && reasoningCapable && !isOpenRouter,
|
|
472
|
+
supportsToolChoice: true,
|
|
473
|
+
supportsForcedToolChoice: true,
|
|
474
|
+
reasoningContentField: "reasoning_content",
|
|
475
|
+
requiresReasoningContentForToolCalls:
|
|
476
|
+
(isKimiModel || (isDeepseekFamily && reasoningCapable) || (isOpenRouter && reasoningCapable)) &&
|
|
477
|
+
reasoningCapable,
|
|
478
|
+
requiresReasoningContentForAllAssistantTurns: isDeepseekFamily && reasoningCapable && !isOpenRouter,
|
|
479
|
+
allowsSyntheticReasoningContentForToolCalls: !isDeepseekFamily || !reasoningCapable,
|
|
480
|
+
requiresThinkingAsText: false,
|
|
481
|
+
requiresMistralToolIds: false,
|
|
482
|
+
requiresToolResultName: false,
|
|
483
|
+
requiresAssistantAfterToolResult: false,
|
|
484
|
+
requiresAssistantContentForToolCalls: isKimiModel,
|
|
485
|
+
openRouterRouting: undefined,
|
|
486
|
+
isOpenRouterHost: isOpenRouter,
|
|
487
|
+
wireModelIdMode: isOpenRouter ? "openrouter" : "raw",
|
|
488
|
+
alwaysSendMaxTokens: spec.id ? isKimiModelId(spec.id) : false,
|
|
489
|
+
enableGeminiThinkingLoopGuard: modelFamilyToken(spec.id ?? "") === "gemini",
|
|
490
|
+
supportsObfuscationOptOut: isOpenAIUrl || spec.provider === "openai",
|
|
491
|
+
stripDeepseekSpecialTokens:
|
|
492
|
+
Boolean(id) && isDeepseekModelIdOrName(id) && (spec.provider === "nvidia" || spec.provider === "deepseek"),
|
|
493
|
+
streamMarkupHealingPattern: id ? detectStreamMarkupHealingPattern(spec.provider, id) : undefined,
|
|
494
|
+
reasoningDeltasMayBeCumulative:
|
|
495
|
+
MINIMAX_PROVIDER_OR_ID_PATTERN.test(spec.provider) || (id ? MINIMAX_PROVIDER_OR_ID_PATTERN.test(id) : false),
|
|
496
|
+
emptyLengthFinishIsContextError: spec.provider === "ollama",
|
|
497
|
+
usesOpenAIToolCallIdLimit: spec.provider === "openai",
|
|
498
|
+
promptCacheSessionHeader: spec.provider === "xai-oauth" ? "x-grok-conv-id" : undefined,
|
|
332
499
|
};
|
|
333
500
|
applyCompatOverrides(compat, spec.compat);
|
|
501
|
+
if (spec.compat?.reasoningDisableMode === undefined) {
|
|
502
|
+
compat.reasoningDisableMode = resolveReasoningDisableMode(compat.thinkingFormat);
|
|
503
|
+
}
|
|
504
|
+
if (spec.compat?.omitReasoningEffort === undefined && !compat.supportsReasoningEffort) {
|
|
505
|
+
compat.omitReasoningEffort = true;
|
|
506
|
+
}
|
|
507
|
+
mergeOllamaReasoningEffortMap(compat, spec.provider, spec.reasoning);
|
|
334
508
|
return compat;
|
|
335
509
|
}
|
|
510
|
+
|
|
511
|
+
type ResponsesOnlyCompat = Omit<ResolvedOpenAIResponsesCompat, keyof ResolvedOpenAISharedCompat>;
|
|
512
|
+
|
|
513
|
+
function pickResponsesOnly(compat: ResolvedOpenAIResponsesCompat): ResponsesOnlyCompat {
|
|
514
|
+
return {
|
|
515
|
+
supportsLongPromptCacheRetention: compat.supportsLongPromptCacheRetention,
|
|
516
|
+
strictResponsesPairing: compat.strictResponsesPairing,
|
|
517
|
+
requiresJuiceZeroHack: compat.requiresJuiceZeroHack,
|
|
518
|
+
supportsObfuscationOptOut: compat.supportsObfuscationOptOut,
|
|
519
|
+
} satisfies ResponsesOnlyCompat;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
export function buildOpenRouterCompat(spec: ModelSpec<"openrouter">): ResolvedOpenRouterCompat {
|
|
523
|
+
const chat = buildOpenAICompat({
|
|
524
|
+
...spec,
|
|
525
|
+
api: "openai-completions",
|
|
526
|
+
} as ModelSpec<"openai-completions">);
|
|
527
|
+
const responses = buildOpenAIResponsesCompat(spec);
|
|
528
|
+
return { ...chat, ...pickResponsesOnly(responses) } as ResolvedOpenRouterCompat;
|
|
529
|
+
}
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type } from "arktype";
|
|
2
2
|
import type { ModelSpec } from "../types";
|
|
3
3
|
import { toPositiveNumber } from "../utils";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
ANTIGRAVITY_VARIANT_COLLAPSE_TABLE,
|
|
6
|
+
collapseEffortVariants,
|
|
7
|
+
type VariantCollapseTable,
|
|
8
|
+
} from "../variant-collapse";
|
|
5
9
|
import { getAntigravityUserAgent } from "../wire/gemini-headers";
|
|
6
10
|
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
] as const;
|
|
11
|
+
export const ANTIGRAVITY_PRIMARY_ENDPOINT = "https://daily-cloudcode-pa.googleapis.com";
|
|
12
|
+
export const ANTIGRAVITY_SANDBOX_ENDPOINT = "https://daily-cloudcode-pa.sandbox.googleapis.com";
|
|
13
|
+
const DEFAULT_ANTIGRAVITY_DISCOVERY_ENDPOINTS = [ANTIGRAVITY_PRIMARY_ENDPOINT, ANTIGRAVITY_SANDBOX_ENDPOINT] as const;
|
|
11
14
|
const FETCH_AVAILABLE_MODELS_PATH = "/v1internal:fetchAvailableModels";
|
|
12
15
|
|
|
13
16
|
const DEFAULT_CONTEXT_WINDOW = 200_000;
|
|
@@ -53,94 +56,78 @@ export interface AntigravityDiscoveryApiResponse {
|
|
|
53
56
|
models?: Record<string, AntigravityDiscoveryApiModel>;
|
|
54
57
|
agentModelSorts?: AntigravityDiscoveryAgentModelSort[];
|
|
55
58
|
}
|
|
56
|
-
const AntigravityDiscoveryApiModelSchema
|
|
57
|
-
.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
),
|
|
129
|
-
agentModelSorts: z.preprocess(
|
|
130
|
-
value => (Array.isArray(value) ? value : undefined),
|
|
131
|
-
z
|
|
132
|
-
.array(z.unknown())
|
|
133
|
-
.transform(sorts =>
|
|
134
|
-
sorts.flatMap(sort => {
|
|
135
|
-
const parsedSort = AntigravityDiscoveryAgentModelSortSchema.safeParse(sort);
|
|
136
|
-
return parsedSort.success ? [parsedSort.data] : [];
|
|
137
|
-
}),
|
|
138
|
-
)
|
|
139
|
-
.optional(),
|
|
140
|
-
),
|
|
141
|
-
})
|
|
142
|
-
.loose();
|
|
143
|
-
|
|
59
|
+
const AntigravityDiscoveryApiModelSchema = type({
|
|
60
|
+
"displayName?": type("unknown").pipe(value => (typeof value === "string" ? value : undefined)),
|
|
61
|
+
"supportsImages?": type("unknown").pipe(value => (typeof value === "boolean" ? value : undefined)),
|
|
62
|
+
"supportsThinking?": type("unknown").pipe(value => (typeof value === "boolean" ? value : undefined)),
|
|
63
|
+
"thinkingBudget?": type("unknown").pipe(value =>
|
|
64
|
+
typeof value === "number" && Number.isFinite(value) ? value : undefined,
|
|
65
|
+
),
|
|
66
|
+
"recommended?": type("unknown").pipe(value => (typeof value === "boolean" ? value : undefined)),
|
|
67
|
+
"maxTokens?": type("unknown").pipe(value =>
|
|
68
|
+
typeof value === "number" && Number.isFinite(value) ? value : undefined,
|
|
69
|
+
),
|
|
70
|
+
"maxOutputTokens?": type("unknown").pipe(value =>
|
|
71
|
+
typeof value === "number" && Number.isFinite(value) ? value : undefined,
|
|
72
|
+
),
|
|
73
|
+
"model?": type("unknown").pipe(value => (typeof value === "string" ? value : undefined)),
|
|
74
|
+
"apiProvider?": type("unknown").pipe(value => (typeof value === "string" ? value : undefined)),
|
|
75
|
+
"modelProvider?": type("unknown").pipe(value => (typeof value === "string" ? value : undefined)),
|
|
76
|
+
"isInternal?": type("unknown").pipe(value => (typeof value === "boolean" ? value : undefined)),
|
|
77
|
+
"supportsVideo?": type("unknown").pipe(value => (typeof value === "boolean" ? value : undefined)),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const AntigravityDiscoveryAgentModelGroupSchema = type({
|
|
81
|
+
"modelIds?": type("unknown").pipe(value =>
|
|
82
|
+
Array.isArray(value) ? value.filter((modelId): modelId is string => typeof modelId === "string") : undefined,
|
|
83
|
+
),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const AntigravityDiscoveryAgentModelSortSchema = type({
|
|
87
|
+
"groups?": type("unknown").pipe(value => {
|
|
88
|
+
if (!Array.isArray(value)) return undefined;
|
|
89
|
+
const result: AntigravityDiscoveryAgentModelGroup[] = [];
|
|
90
|
+
for (const group of value) {
|
|
91
|
+
const parsedGroup = AntigravityDiscoveryAgentModelGroupSchema(group);
|
|
92
|
+
if (!(parsedGroup instanceof type.errors)) {
|
|
93
|
+
result.push(parsedGroup);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return result;
|
|
97
|
+
}),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const AntigravityDiscoveryApiResponseSchema = type({
|
|
101
|
+
"models?": type("unknown").pipe(value => {
|
|
102
|
+
if (typeof value !== "object" || value === null) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
const normalized: Record<string, AntigravityDiscoveryApiModel> = {};
|
|
106
|
+
for (const [modelId, modelValue] of Object.entries(value)) {
|
|
107
|
+
if (typeof modelValue !== "object" || modelValue === null) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const parsedModel = AntigravityDiscoveryApiModelSchema(modelValue);
|
|
111
|
+
if (!(parsedModel instanceof type.errors)) {
|
|
112
|
+
normalized[modelId] = parsedModel;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return normalized;
|
|
116
|
+
}),
|
|
117
|
+
"agentModelSorts?": type("unknown").pipe(value => {
|
|
118
|
+
if (!Array.isArray(value)) {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
const result: AntigravityDiscoveryAgentModelSort[] = [];
|
|
122
|
+
for (const sort of value) {
|
|
123
|
+
const parsedSort = AntigravityDiscoveryAgentModelSortSchema(sort);
|
|
124
|
+
if (!(parsedSort instanceof type.errors)) {
|
|
125
|
+
result.push(parsedSort);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return result;
|
|
129
|
+
}),
|
|
130
|
+
});
|
|
144
131
|
/**
|
|
145
132
|
* Options for fetching Antigravity discovery models.
|
|
146
133
|
*/
|
|
@@ -157,6 +144,12 @@ export interface FetchAntigravityDiscoveryModelsOptions {
|
|
|
157
144
|
signal?: AbortSignal;
|
|
158
145
|
/** Optional fetch implementation override for tests. */
|
|
159
146
|
fetcher?: typeof fetch;
|
|
147
|
+
/**
|
|
148
|
+
* Hand collapse table to apply to the discovered list. Defaults to the
|
|
149
|
+
* Antigravity (budget-transport) table; `googleGeminiCli` passes the
|
|
150
|
+
* level-transport table so cloudcode-pa keeps `thinkingLevel`.
|
|
151
|
+
*/
|
|
152
|
+
collapseTable?: VariantCollapseTable;
|
|
160
153
|
}
|
|
161
154
|
|
|
162
155
|
/**
|
|
@@ -239,7 +232,7 @@ export async function fetchAntigravityDiscoveryModels(
|
|
|
239
232
|
// Collapse effort-tier variants at the source so runtime discovery,
|
|
240
233
|
// the gemini-cli re-provision, and the catalog generator all see
|
|
241
234
|
// logical ids only.
|
|
242
|
-
const collapsed = collapseEffortVariants(models, ANTIGRAVITY_VARIANT_COLLAPSE_TABLE);
|
|
235
|
+
const collapsed = collapseEffortVariants(models, options.collapseTable ?? ANTIGRAVITY_VARIANT_COLLAPSE_TABLE);
|
|
243
236
|
collapsed.sort((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id));
|
|
244
237
|
return collapsed;
|
|
245
238
|
}
|
|
@@ -248,11 +241,11 @@ export async function fetchAntigravityDiscoveryModels(
|
|
|
248
241
|
}
|
|
249
242
|
|
|
250
243
|
function parseAntigravityDiscoveryResponse(value: unknown): AntigravityDiscoveryApiResponse | null {
|
|
251
|
-
const parsed = AntigravityDiscoveryApiResponseSchema
|
|
252
|
-
if (
|
|
244
|
+
const parsed = AntigravityDiscoveryApiResponseSchema(value);
|
|
245
|
+
if (parsed instanceof type.errors) {
|
|
253
246
|
return null;
|
|
254
247
|
}
|
|
255
|
-
return parsed
|
|
248
|
+
return parsed;
|
|
256
249
|
}
|
|
257
250
|
|
|
258
251
|
function trimTrailingSlashes(value: string): string {
|
package/src/discovery/codex.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type } from "arktype";
|
|
2
2
|
import type { ModelSpec } from "../types";
|
|
3
3
|
import { isRecord } from "../utils";
|
|
4
4
|
import { CODEX_BASE_URL, OPENAI_HEADER_VALUES, OPENAI_HEADERS } from "../wire/codex";
|
|
@@ -9,36 +9,29 @@ const DEFAULT_MAX_TOKENS = 128_000;
|
|
|
9
9
|
const DEFAULT_CODEX_CLIENT_VERSION = "0.99.0";
|
|
10
10
|
const NPM_CODEX_LATEST_URL = "https://registry.npmjs.org/@openai%2Fcodex/latest";
|
|
11
11
|
|
|
12
|
-
const codexReasoningPresetSchema =
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
models: z.array(z.unknown()).optional(),
|
|
36
|
-
data: z.array(z.unknown()).optional(),
|
|
37
|
-
})
|
|
38
|
-
.loose();
|
|
39
|
-
|
|
40
|
-
type CodexModelEntry = z.infer<typeof codexModelEntrySchema>;
|
|
41
|
-
|
|
12
|
+
const codexReasoningPresetSchema = type({
|
|
13
|
+
"effort?": "unknown",
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const codexModelEntrySchema = type({
|
|
17
|
+
"slug?": "unknown",
|
|
18
|
+
"id?": "unknown",
|
|
19
|
+
"display_name?": "unknown",
|
|
20
|
+
"context_window?": "unknown",
|
|
21
|
+
"default_reasoning_level?": "unknown",
|
|
22
|
+
"supported_reasoning_levels?": "unknown",
|
|
23
|
+
"input_modalities?": "unknown",
|
|
24
|
+
"supported_in_api?": "unknown",
|
|
25
|
+
"priority?": "unknown",
|
|
26
|
+
"prefer_websockets?": "unknown",
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const codexModelsResponseSchema = type({
|
|
30
|
+
"models?": "unknown[]",
|
|
31
|
+
"data?": "unknown[]",
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
type CodexModelEntry = typeof codexModelEntrySchema.infer;
|
|
42
35
|
interface NormalizedCodexModel {
|
|
43
36
|
model: ModelSpec<"openai-codex-responses">;
|
|
44
37
|
priority: number;
|
|
@@ -216,12 +209,12 @@ function isAbortError(error: unknown): error is Error {
|
|
|
216
209
|
}
|
|
217
210
|
|
|
218
211
|
function normalizeCodexModels(payload: unknown, baseUrl: string): ModelSpec<"openai-codex-responses">[] | null {
|
|
219
|
-
const parsedResponse = codexModelsResponseSchema
|
|
220
|
-
if (
|
|
212
|
+
const parsedResponse = codexModelsResponseSchema(payload);
|
|
213
|
+
if (parsedResponse instanceof type.errors) {
|
|
221
214
|
return null;
|
|
222
215
|
}
|
|
223
216
|
|
|
224
|
-
const entries = parsedResponse.
|
|
217
|
+
const entries = parsedResponse.models ?? parsedResponse.data ?? [];
|
|
225
218
|
const normalized: NormalizedCodexModel[] = [];
|
|
226
219
|
for (const entry of entries) {
|
|
227
220
|
const model = normalizeCodexModelEntry(entry, baseUrl);
|
|
@@ -241,12 +234,12 @@ function normalizeCodexModels(payload: unknown, baseUrl: string): ModelSpec<"ope
|
|
|
241
234
|
}
|
|
242
235
|
|
|
243
236
|
function normalizeCodexModelEntry(entry: unknown, baseUrl: string): NormalizedCodexModel | null {
|
|
244
|
-
const parsedEntry = codexModelEntrySchema
|
|
245
|
-
if (
|
|
237
|
+
const parsedEntry = codexModelEntrySchema(entry);
|
|
238
|
+
if (parsedEntry instanceof type.errors) {
|
|
246
239
|
return null;
|
|
247
240
|
}
|
|
248
241
|
|
|
249
|
-
const payload: CodexModelEntry = parsedEntry
|
|
242
|
+
const payload: CodexModelEntry = parsedEntry;
|
|
250
243
|
const slug = toNonEmptyString(payload.slug) ?? toNonEmptyString(payload.id);
|
|
251
244
|
if (!slug) {
|
|
252
245
|
return null;
|
|
@@ -295,11 +288,11 @@ function supportsReasoning(defaultReasoningLevel: unknown, supportedReasoningLev
|
|
|
295
288
|
}
|
|
296
289
|
|
|
297
290
|
for (const level of supportedReasoningLevels) {
|
|
298
|
-
const parsedLevel = codexReasoningPresetSchema
|
|
299
|
-
if (
|
|
291
|
+
const parsedLevel = codexReasoningPresetSchema(level);
|
|
292
|
+
if (parsedLevel instanceof type.errors) {
|
|
300
293
|
continue;
|
|
301
294
|
}
|
|
302
|
-
const effort = toNonEmptyString(parsedLevel.
|
|
295
|
+
const effort = toNonEmptyString(parsedLevel.effort)?.toLowerCase();
|
|
303
296
|
if (effort && effort !== "none") {
|
|
304
297
|
return true;
|
|
305
298
|
}
|