@oh-my-pi/pi-catalog 16.0.5 → 16.0.7

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.
@@ -13,13 +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,
20
21
  modelFamilyToken,
21
22
  } from "../identity/family";
22
- import type { ModelSpec, OpenAICompat, ResolvedOpenAICompat, ResolvedOpenAIResponsesCompat } from "../types";
23
+ import type {
24
+ ModelSpec,
25
+ OpenAICompat,
26
+ OpenAIStreamMarkupHealingPattern,
27
+ ResolvedOpenAICompat,
28
+ ResolvedOpenAIResponsesCompat,
29
+ ResolvedOpenAISharedCompat,
30
+ ResolvedOpenRouterCompat,
31
+ } from "../types";
23
32
  import { applyCompatOverrides } from "./apply";
24
33
 
25
34
  /** GLM coding-plan SKUs idle for minutes mid-reasoning; see `streamIdleTimeoutMs`. */
@@ -29,6 +38,76 @@ const GLM_CODING_PLAN_STREAM_IDLE_TIMEOUT_MS = 600_000;
29
38
  const DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS = 300_000;
30
39
  /** Kimi K2.6 can spend several minutes reasoning before the first visible token. */
31
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
+ }
32
111
 
33
112
  /**
34
113
  * OpenCode's gateways (https://opencode.ai/zen|go) gate `reasoning_content`
@@ -197,6 +276,25 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
197
276
  ? DEEPSEEK_REASONING_STREAM_IDLE_TIMEOUT_MS
198
277
  : undefined;
199
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
+
200
298
  const compat: ResolvedOpenAICompat = {
201
299
  supportsStore: !isNonStandard,
202
300
  // `developer` is an OpenAI-Responses-era extension to the chat-completions schema. Almost
@@ -229,7 +327,7 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
229
327
  supportsForcedToolChoice: true,
230
328
  maxTokensField: useMaxTokens ? "max_tokens" : "max_completion_tokens",
231
329
  requiresToolResultName: isMistral,
232
- requiresAssistantAfterToolResult: false,
330
+ requiresAssistantAfterToolResult: isMistral,
233
331
  requiresThinkingAsText: isMistral,
234
332
  requiresMistralToolIds: isMistral,
235
333
  // Only Kimi's native hosts (Moonshot / Kimi-code, matched by `isMoonshotKimi`)
@@ -241,16 +339,11 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
241
339
  // (`chat_template_kwargs.enable_thinking`); top-level `enable_thinking`
242
340
  // is rejected by NIM's `additionalProperties: false` request schema
243
341
  // (issue #2299).
244
- thinkingFormat:
245
- isZai || isZhipu || isMoonshotKimi || isXiaomiMimo
246
- ? "zai"
247
- : isOpenRouter
248
- ? "openrouter"
249
- : isQwen && isNvidiaNim
250
- ? "qwen-chat-template"
251
- : isAlibaba || isQwen
252
- ? "qwen"
253
- : "openai",
342
+ thinkingFormat,
343
+ reasoningDisableMode: resolveReasoningDisableMode(thinkingFormat),
344
+ omitReasoningEffort: false,
345
+ includeEncryptedReasoning: true,
346
+ filterReasoningHistory: false,
254
347
  thinkingKeep: usesMoonshotKimiPreservedThinking ? "all" : undefined,
255
348
  reasoningContentField: "reasoning_content",
256
349
  // Backends that 400 follow-up requests when prior assistant tool-call turns lack `reasoning_content`:
@@ -271,6 +364,8 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
271
364
  (isDeepseekFamily && Boolean(spec.reasoning)) ||
272
365
  isXiaomiMimo ||
273
366
  (isOpenRouter && Boolean(spec.reasoning)),
367
+ requiresReasoningContentForAllAssistantTurns:
368
+ ((isDeepseekFamily && Boolean(spec.reasoning)) || isXiaomiMimo) && !isOpenRouter,
274
369
  // DeepSeek V4 and Xiaomi MiMo reject synthetic reasoning_content placeholders (".") on tool-call turns.
275
370
  // Kimi and OpenRouter accept them when actual reasoning is unavailable.
276
371
  allowsSyntheticReasoningContentForToolCalls: (!isDeepseekFamily || !spec.reasoning) && !isXiaomiMimo,
@@ -279,20 +374,45 @@ export function buildOpenAICompat(spec: ModelSpec<"openai-completions">): Resolv
279
374
  openRouterRouting: undefined,
280
375
  vercelGatewayRouting: undefined,
281
376
  isOpenRouterHost: isOpenRouter,
377
+ wireModelIdMode,
282
378
  isVercelGatewayHost: isVercelGateway,
283
379
  supportsStrictMode: detectStrictModeSupport(provider, baseUrl),
284
380
  extraBody: isDirectDeepseekReasoning ? { thinking: { type: "enabled" } } : undefined,
285
381
  toolStrictMode: isCerebras ? "all_strict" : "mixed",
382
+ toolSchemaFlavor: isMoonshotNative ? "moonshot-mfjs" : undefined,
286
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",
287
393
  };
288
394
 
289
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);
290
403
 
291
404
  const whenThinkingPolicy =
292
405
  spec.compat?.whenThinking ?? (isOpenCodeProvider && spec.reasoning ? OPENCODE_WHEN_THINKING : undefined);
293
406
  if (whenThinkingPolicy) {
294
407
  const variant: ResolvedOpenAICompat = { ...compat };
295
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);
296
416
  compat.whenThinking = variant;
297
417
  }
298
418
 
@@ -304,6 +424,7 @@ interface OpenAIResponsesSpecLike {
304
424
  provider: string;
305
425
  name: string;
306
426
  baseUrl: string;
427
+ reasoning?: boolean;
307
428
  compat?: OpenAICompat;
308
429
  }
309
430
 
@@ -321,22 +442,88 @@ interface OpenAIResponsesSpecLike {
321
442
  export function buildOpenAIResponsesCompat(spec: OpenAIResponsesSpecLike): ResolvedOpenAIResponsesCompat {
322
443
  const baseUrl = spec.baseUrl ?? "";
323
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
+
324
453
  const compat: ResolvedOpenAIResponsesCompat = {
325
- supportsDeveloperRole: isAzure || hostMatchesUrl(baseUrl, "openai") || hostMatchesUrl(baseUrl, "githubCopilot"),
454
+ supportsDeveloperRole: isAzure || isOpenAIUrl || hostMatchesUrl(baseUrl, "githubCopilot"),
326
455
  supportsStrictMode:
327
- spec.provider === "openai" ||
328
- isAzure ||
329
- spec.provider === "github-copilot" ||
330
- hostMatchesUrl(baseUrl, "openai"),
331
- supportsReasoningEffort: true,
332
- 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,
333
459
  // Azure OpenAI and GitHub Copilot Responses paths require tool results
334
460
  // to strictly match prior tool calls when building Responses inputs.
335
461
  strictResponsesPairing: isAzure || spec.provider === "github-copilot",
336
462
  requiresJuiceZeroHack: spec.name.toLowerCase().startsWith("gpt-5"),
337
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,
338
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,
339
499
  };
340
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);
341
508
  return compat;
342
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,4 +1,4 @@
1
- import { z } from "zod/v4";
1
+ import { type } from "arktype";
2
2
  import type { ModelSpec } from "../types";
3
3
  import { toPositiveNumber } from "../utils";
4
4
  import {
@@ -56,94 +56,78 @@ export interface AntigravityDiscoveryApiResponse {
56
56
  models?: Record<string, AntigravityDiscoveryApiModel>;
57
57
  agentModelSorts?: AntigravityDiscoveryAgentModelSort[];
58
58
  }
59
- const AntigravityDiscoveryApiModelSchema: z.ZodType<AntigravityDiscoveryApiModel> = z
60
- .object({
61
- displayName: z.preprocess(value => (typeof value === "string" ? value : undefined), z.string().optional()),
62
- supportsImages: z.preprocess(value => (typeof value === "boolean" ? value : undefined), z.boolean().optional()),
63
- supportsThinking: z.preprocess(value => (typeof value === "boolean" ? value : undefined), z.boolean().optional()),
64
- thinkingBudget: z.preprocess(
65
- value => (typeof value === "number" && Number.isFinite(value) ? value : undefined),
66
- z.number().optional(),
67
- ),
68
- recommended: z.preprocess(value => (typeof value === "boolean" ? value : undefined), z.boolean().optional()),
69
- maxTokens: z.preprocess(
70
- value => (typeof value === "number" && Number.isFinite(value) ? value : undefined),
71
- z.number().optional(),
72
- ),
73
- maxOutputTokens: z.preprocess(
74
- value => (typeof value === "number" && Number.isFinite(value) ? value : undefined),
75
- z.number().optional(),
76
- ),
77
- model: z.preprocess(value => (typeof value === "string" ? value : undefined), z.string().optional()),
78
- apiProvider: z.preprocess(value => (typeof value === "string" ? value : undefined), z.string().optional()),
79
- modelProvider: z.preprocess(value => (typeof value === "string" ? value : undefined), z.string().optional()),
80
- isInternal: z.preprocess(value => (typeof value === "boolean" ? value : undefined), z.boolean().optional()),
81
- supportsVideo: z.preprocess(value => (typeof value === "boolean" ? value : undefined), z.boolean().optional()),
82
- })
83
- .loose();
84
- const AntigravityDiscoveryAgentModelGroupSchema: z.ZodType<AntigravityDiscoveryAgentModelGroup> = z
85
- .object({
86
- modelIds: z.preprocess(
87
- value =>
88
- Array.isArray(value)
89
- ? value.filter((modelId): modelId is string => typeof modelId === "string")
90
- : undefined,
91
- z.array(z.string()).optional(),
92
- ),
93
- })
94
- .loose();
95
- const AntigravityDiscoveryAgentModelSortSchema: z.ZodType<AntigravityDiscoveryAgentModelSort> = z
96
- .object({
97
- groups: z.preprocess(
98
- value => (Array.isArray(value) ? value : undefined),
99
- z
100
- .array(z.unknown())
101
- .transform(groups =>
102
- groups.flatMap(group => {
103
- const parsedGroup = AntigravityDiscoveryAgentModelGroupSchema.safeParse(group);
104
- return parsedGroup.success ? [parsedGroup.data] : [];
105
- }),
106
- )
107
- .optional(),
108
- ),
109
- })
110
- .loose();
111
- const AntigravityDiscoveryApiResponseSchema: z.ZodType<AntigravityDiscoveryApiResponse> = z
112
- .object({
113
- models: z.preprocess(
114
- value => (typeof value === "object" && value !== null ? value : undefined),
115
- z
116
- .record(z.string(), z.unknown())
117
- .transform(models => {
118
- const normalized: Record<string, AntigravityDiscoveryApiModel> = {};
119
- for (const [modelId, modelValue] of Object.entries(models)) {
120
- if (typeof modelValue !== "object" || modelValue === null) {
121
- continue;
122
- }
123
- const parsedModel = AntigravityDiscoveryApiModelSchema.safeParse(modelValue);
124
- if (parsedModel.success) {
125
- normalized[modelId] = parsedModel.data;
126
- }
127
- }
128
- return normalized;
129
- })
130
- .optional(),
131
- ),
132
- agentModelSorts: z.preprocess(
133
- value => (Array.isArray(value) ? value : undefined),
134
- z
135
- .array(z.unknown())
136
- .transform(sorts =>
137
- sorts.flatMap(sort => {
138
- const parsedSort = AntigravityDiscoveryAgentModelSortSchema.safeParse(sort);
139
- return parsedSort.success ? [parsedSort.data] : [];
140
- }),
141
- )
142
- .optional(),
143
- ),
144
- })
145
- .loose();
146
-
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
+ });
147
131
  /**
148
132
  * Options for fetching Antigravity discovery models.
149
133
  */
@@ -257,11 +241,11 @@ export async function fetchAntigravityDiscoveryModels(
257
241
  }
258
242
 
259
243
  function parseAntigravityDiscoveryResponse(value: unknown): AntigravityDiscoveryApiResponse | null {
260
- const parsed = AntigravityDiscoveryApiResponseSchema.safeParse(value);
261
- if (!parsed.success) {
244
+ const parsed = AntigravityDiscoveryApiResponseSchema(value);
245
+ if (parsed instanceof type.errors) {
262
246
  return null;
263
247
  }
264
- return parsed.data;
248
+ return parsed;
265
249
  }
266
250
 
267
251
  function trimTrailingSlashes(value: string): string {
@@ -1,4 +1,4 @@
1
- import { z } from "zod/v4";
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 = z
13
- .object({
14
- effort: z.unknown().optional(),
15
- })
16
- .loose();
17
-
18
- const codexModelEntrySchema = z
19
- .object({
20
- slug: z.unknown().optional(),
21
- id: z.unknown().optional(),
22
- display_name: z.unknown().optional(),
23
- context_window: z.unknown().optional(),
24
- default_reasoning_level: z.unknown().optional(),
25
- supported_reasoning_levels: z.unknown().optional(),
26
- input_modalities: z.unknown().optional(),
27
- supported_in_api: z.unknown().optional(),
28
- priority: z.unknown().optional(),
29
- prefer_websockets: z.unknown().optional(),
30
- })
31
- .loose();
32
-
33
- const codexModelsResponseSchema = z
34
- .object({
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.safeParse(payload);
220
- if (!parsedResponse.success) {
212
+ const parsedResponse = codexModelsResponseSchema(payload);
213
+ if (parsedResponse instanceof type.errors) {
221
214
  return null;
222
215
  }
223
216
 
224
- const entries = parsedResponse.data.models ?? parsedResponse.data.data ?? [];
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.safeParse(entry);
245
- if (!parsedEntry.success) {
237
+ const parsedEntry = codexModelEntrySchema(entry);
238
+ if (parsedEntry instanceof type.errors) {
246
239
  return null;
247
240
  }
248
241
 
249
- const payload: CodexModelEntry = parsedEntry.data;
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.safeParse(level);
299
- if (!parsedLevel.success) {
291
+ const parsedLevel = codexReasoningPresetSchema(level);
292
+ if (parsedLevel instanceof type.errors) {
300
293
  continue;
301
294
  }
302
- const effort = toNonEmptyString(parsedLevel.data.effort)?.toLowerCase();
295
+ const effort = toNonEmptyString(parsedLevel.effort)?.toLowerCase();
303
296
  if (effort && effort !== "none") {
304
297
  return true;
305
298
  }