@bitkyc08/opencodex 2.29.0 → 2.31.0-preview.20260822

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.
Files changed (68) hide show
  1. package/README.md +5 -5
  2. package/gui/dist/assets/index-DyWYnr-t.js +102 -0
  3. package/gui/dist/index.html +1 -1
  4. package/package.json +3 -3
  5. package/src/adapters/cursor/cursor-errors.ts +65 -6
  6. package/src/adapters/cursor/discovery.ts +29 -2
  7. package/src/adapters/cursor/effort-map.ts +6 -0
  8. package/src/adapters/cursor/h2-pool.ts +123 -0
  9. package/src/adapters/cursor/images.ts +704 -0
  10. package/src/adapters/cursor/live-models.ts +21 -26
  11. package/src/adapters/cursor/live-transport.ts +239 -8
  12. package/src/adapters/cursor/native-exec-common.ts +17 -0
  13. package/src/adapters/cursor/native-exec.ts +9 -4
  14. package/src/adapters/cursor/protobuf-events.ts +5 -1
  15. package/src/adapters/cursor/protobuf-request.ts +46 -9
  16. package/src/adapters/cursor/request-builder.ts +29 -14
  17. package/src/adapters/cursor/tool-definitions.ts +20 -0
  18. package/src/adapters/cursor/transport.ts +10 -0
  19. package/src/adapters/cursor/types.ts +8 -1
  20. package/src/adapters/cursor.ts +23 -5
  21. package/src/adapters/google.ts +16 -3
  22. package/src/adapters/openai-responses.ts +66 -20
  23. package/src/adapters/xai-web-search.ts +185 -0
  24. package/src/cli/agent.ts +2 -1
  25. package/src/cli/dispatch.ts +2 -2
  26. package/src/cli/doctor.ts +89 -0
  27. package/src/cli/help.ts +2 -0
  28. package/src/cli/registry.ts +7 -2
  29. package/src/codex/auth-context.ts +41 -2
  30. package/src/codex/catalog/effort.ts +1 -1
  31. package/src/codex/catalog/parsing.ts +2 -0
  32. package/src/codex/catalog/provider-fetch.ts +20 -5
  33. package/src/codex/coordinator-doctor.ts +332 -0
  34. package/src/codex/features.ts +58 -0
  35. package/src/codex/inject-coordination.ts +39 -6
  36. package/src/codex/transition-state.ts +12 -12
  37. package/src/generated/compatibility-version.json +86 -58
  38. package/src/lib/bun-stream-caps.ts +7 -4
  39. package/src/lib/errors.ts +8 -2
  40. package/src/oauth/cursor.ts +21 -0
  41. package/src/providers/command-code-efforts.ts +7 -0
  42. package/src/providers/cursor-pool.ts +72 -0
  43. package/src/providers/derive.ts +3 -0
  44. package/src/providers/fastwire.ts +12 -1
  45. package/src/providers/openai-sidecar.ts +1 -0
  46. package/src/providers/quota.ts +98 -25
  47. package/src/providers/registry.ts +115 -10
  48. package/src/providers/service-tier.ts +22 -7
  49. package/src/responses/custom-tool-compat.ts +24 -8
  50. package/src/responses/namespace-tool-compat.ts +2 -3
  51. package/src/router.ts +3 -0
  52. package/src/server/chat-completions.ts +4 -0
  53. package/src/server/chat-native.ts +20 -0
  54. package/src/server/management/agent-settings-routes.ts +16 -5
  55. package/src/server/management/config-routes.ts +25 -5
  56. package/src/server/management/vision-sidecar-options.ts +54 -19
  57. package/src/server/responses/compact.ts +1 -2
  58. package/src/server/responses/core.ts +54 -13
  59. package/src/service.ts +122 -14
  60. package/src/types/config.ts +9 -3
  61. package/src/types/provider.ts +6 -0
  62. package/src/usage/cost.ts +52 -38
  63. package/src/usage/expected-prices.ts +79 -9
  64. package/src/vision/backends.ts +97 -0
  65. package/src/vision/eligibility.ts +43 -22
  66. package/src/vision/index.ts +73 -5
  67. package/src/vision/routed-describe.ts +175 -0
  68. package/gui/dist/assets/index-BNESwCzn.js +0 -102
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Weighted credential routing for Cursor accounts.
3
+ *
4
+ * Transfer from yelixir-dev/cursor-ai-proxy-bridge credentials.ts:
5
+ * weighted round-robin selection with per-credential auth-failure cooldown
6
+ * and one-retry failover on a different account before surfacing the error.
7
+ *
8
+ * OpenCodex already has JWT-based multi-account identification (src/oauth/cursor.ts)
9
+ * and Anthropic-specific 429 rotation; this module adds Cursor-aware weighted
10
+ * routing on top of those primitives.
11
+ */
12
+
13
+ export interface CursorCredential {
14
+ readonly id: string;
15
+ weight: number;
16
+ }
17
+
18
+ interface CredentialState {
19
+ readonly credential: CursorCredential;
20
+ currentWeight: number;
21
+ disabledUntil: number;
22
+ }
23
+
24
+ export class NoAvailableCursorCredentialError extends Error {
25
+ constructor(message = "No available Cursor credentials") { super(message); }
26
+ }
27
+
28
+ export class CursorCredentialRouter {
29
+ private states: CredentialState[] = [];
30
+ private readonly cooldownMs: number;
31
+
32
+ constructor(credentials: ReadonlyArray<CursorCredential>, cooldownMs = 300_000) {
33
+ this.cooldownMs = cooldownMs;
34
+ this.replace(credentials);
35
+ }
36
+
37
+ replace(credentials: ReadonlyArray<CursorCredential>): void {
38
+ this.states = credentials.map(c => ({
39
+ credential: { ...c, weight: Math.max(1, c.weight || 1) },
40
+ currentWeight: 0,
41
+ disabledUntil: 0,
42
+ }));
43
+ }
44
+
45
+ pick(excludeIds: ReadonlySet<string> = new Set()): CursorCredential {
46
+ const now = Date.now();
47
+ const candidates = this.states.filter(s =>
48
+ !excludeIds.has(s.credential.id) && s.disabledUntil <= now,
49
+ );
50
+ if (candidates.length === 0) throw new NoAvailableCursorCredentialError();
51
+ let selected: CredentialState | undefined;
52
+ let totalWeight = 0;
53
+ for (const state of candidates) {
54
+ state.currentWeight += state.credential.weight;
55
+ totalWeight += state.credential.weight;
56
+ if (!selected || state.currentWeight > selected.currentWeight) selected = state;
57
+ }
58
+ if (!selected) throw new NoAvailableCursorCredentialError();
59
+ selected.currentWeight -= totalWeight;
60
+ return { ...selected.credential };
61
+ }
62
+
63
+ disable(id: string): void {
64
+ const state = this.states.find(s => s.credential.id === id);
65
+ if (state) state.disabledUntil = Date.now() + this.cooldownMs;
66
+ }
67
+
68
+ get snapshot(): ReadonlyArray<{ id: string; disabled: boolean }> {
69
+ const now = Date.now();
70
+ return this.states.map(s => ({ id: s.credential.id, disabled: s.disabledUntil > now }));
71
+ }
72
+ }
@@ -483,6 +483,9 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
483
483
  if (prov.supportsOpenAiWebSearchToolFields === undefined && entry.supportsOpenAiWebSearchToolFields !== undefined) {
484
484
  prov.supportsOpenAiWebSearchToolFields = entry.supportsOpenAiWebSearchToolFields;
485
485
  }
486
+ if (prov.supportsResponsesCustomTools === undefined && entry.supportsResponsesCustomTools !== undefined) {
487
+ prov.supportsResponsesCustomTools = entry.supportsResponsesCustomTools;
488
+ }
486
489
  if (prov.preserveResponsesReasoningContent === undefined && entry.preserveResponsesReasoningContent !== undefined) prov.preserveResponsesReasoningContent = entry.preserveResponsesReasoningContent;
487
490
  applyReasoningSummaryDefaults(prov, entry.modelSupportsReasoningSummaries);
488
491
  applyServiceTierModelDefaults(prov, serviceTierModelDefaultsFor(entry, prov));
@@ -33,6 +33,7 @@ export interface FastPolicyAuthority {
33
33
  readonly providerAdapter: string;
34
34
  readonly providerAuthMode?: ProviderAuthKind;
35
35
  readonly fastWireDeclaration: FastWire | null | undefined;
36
+ readonly fastTierDescription?: string;
36
37
  readonly modelWireOverrideAllowed: boolean;
37
38
  readonly authTransport: FastPolicyAuthTransport;
38
39
  readonly capability: {
@@ -55,6 +56,7 @@ export interface ResolvedFastPolicy {
55
56
  | "pin-unavailable";
56
57
  readonly adapter: string;
57
58
  readonly fastWire: FastWire | null;
59
+ readonly fastTierDescription?: string;
58
60
  readonly forwardCallerTier: boolean;
59
61
  }
60
62
 
@@ -222,7 +224,16 @@ export function resolveFastPolicy(
222
224
  else if (capability === undefined) eligibility = "unclassified";
223
225
  else eligibility = "eligible";
224
226
 
225
- return { capability, eligibility, adapter, fastWire, forwardCallerTier };
227
+ return {
228
+ capability,
229
+ eligibility,
230
+ adapter,
231
+ fastWire,
232
+ ...(authority.fastTierDescription !== undefined
233
+ ? { fastTierDescription: authority.fastTierDescription }
234
+ : {}),
235
+ forwardCallerTier,
236
+ };
226
237
  }
227
238
 
228
239
  export function canonicalFastTierMarker(callerTier: string | undefined): "priority" | undefined {
@@ -169,6 +169,7 @@ export async function resolveFirstUsableOpenAiSidecar(
169
169
  authContext.accountId,
170
170
  outcome,
171
171
  {
172
+ threadId: authContext.affinityKey,
172
173
  probeLeaseId: authContext.probeLeaseId,
173
174
  writerGeneration: authContext.writerGeneration,
174
175
  },
@@ -50,6 +50,7 @@ const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
50
50
  const DEEPSEEK_BASE_URL = "https://api.deepseek.com";
51
51
  const CLINE_BASE_URL = "https://api.cline.bot";
52
52
  const ZAI_BASE_URL = "https://api.z.ai";
53
+ const ZAI_CN_BASE_URL = "https://open.bigmodel.cn";
53
54
  const MINIMAX_REMAINS_URL = "https://www.minimax.io/v1/token_plan/remains";
54
55
  const MOONSHOT_BASE_URL = "https://api.moonshot.ai/v1";
55
56
  const VENICE_BASE_URL = "https://api.venice.ai/api/v1";
@@ -343,7 +344,12 @@ function isCanonicalClineBaseUrl(baseUrl: string): boolean {
343
344
 
344
345
  function isCanonicalZaiBaseUrl(baseUrl: string): boolean {
345
346
  const normalized = normalizedBaseUrl(baseUrl);
346
- return normalized === ZAI_BASE_URL || normalized === `${ZAI_BASE_URL}/api/coding/paas/v4`;
347
+ return normalized === ZAI_BASE_URL
348
+ || normalized === `${ZAI_BASE_URL}/api/coding/paas/v4`
349
+ || normalized === ZAI_CN_BASE_URL
350
+ || normalized === `${ZAI_CN_BASE_URL}/api/coding/paas/v4`
351
+ // BigModel serves the same GLM Coding Plan on the OpenAI Responses wire at /api/v1.
352
+ || normalized === `${ZAI_CN_BASE_URL}/api/v1`;
347
353
  }
348
354
 
349
355
  function isCanonicalMinimaxBaseUrl(baseUrl: string): boolean {
@@ -669,34 +675,67 @@ async function fetchClineQuota(provider: string, config: OcxProviderConfig): Pro
669
675
 
670
676
  /**
671
677
  * Z.AI GLM Coding Plan `GET /api/monitor/usage/quota/limit` — the coding-plan
672
- * subscription's 5-hour token cycle, weekly quota, and monthly MCP usage.
673
- * Authenticates with the API key as a Bearer token per Z.AI's API reference.
678
+ * limits arrive as a `limits` array of `TOKENS_LIMIT` (newer plans call the
679
+ * same rows `CREDIT_LIMIT`) and `TIME_LIMIT` rows. `TOKENS_LIMIT`/`CREDIT_LIMIT`
680
+ * rows carry the window length as `unit`/`number`: unit 3 is hours (number 5 →
681
+ * the rolling five-hour window), unit 6 is weeks (number 1 → the weekly
682
+ * window). `TIME_LIMIT` rows are the monthly MCP tool budget (Web Search / Web
683
+ * Reader / Zread). Every row's `percentage` is the consumed share (falling
684
+ * back to `currentValue`/`usage` when absent) and `nextResetTime` (unix ms)
685
+ * the window reset.
674
686
  */
675
- async function fetchZaiQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
676
- if (!isCanonicalZaiBaseUrl(config.baseUrl)) return null;
677
- const apiKey = resolveEnvValue(config.apiKey)?.trim();
678
- if (!apiKey) return null;
679
- const response = await fetch(`${ZAI_BASE_URL}/api/monitor/usage/quota/limit`, {
680
- headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
681
- redirect: "error",
682
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
683
- });
684
- if (!response.ok) {
685
- return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
686
- ? TERMINAL_QUOTA_FAILURE
687
- : null;
687
+ export function parseZaiQuotaLimits(data: Record<string, unknown> | null): ProviderQuota | null {
688
+ const limits = Array.isArray(data?.limits) ? data.limits as unknown[] : null;
689
+ if (!limits) return null;
690
+ const quota: ProviderQuota = { updatedAt: Date.now() };
691
+ let windows = 0;
692
+ for (const raw of limits) {
693
+ const row = asRecord(raw);
694
+ if (!row) continue;
695
+ const resetAt = normalizeResetAt(row.nextResetTime);
696
+ let percent = normalizePercent(row.percentage);
697
+ if (percent === undefined) {
698
+ const used = toFiniteNumber(row.currentValue);
699
+ const total = toFiniteNumber(row.usage);
700
+ if (used !== undefined && total !== undefined && total > 0) {
701
+ percent = normalizePercent((used / total) * 100);
702
+ }
703
+ }
704
+ if (percent === undefined) continue;
705
+ if (row.type === "TOKENS_LIMIT" || row.type === "CREDIT_LIMIT") {
706
+ const unit = toFiniteNumber(row.unit);
707
+ const number = toFiniteNumber(row.number);
708
+ if (unit === 3 && number === 5) {
709
+ quota.fiveHourPercent = percent;
710
+ if (resetAt !== undefined) quota.fiveHourResetAt = resetAt;
711
+ windows += 1;
712
+ } else if (unit === 6 && number === 1) {
713
+ quota.weeklyPercent = percent;
714
+ if (resetAt !== undefined) quota.weeklyResetAt = resetAt;
715
+ windows += 1;
716
+ }
717
+ } else if (row.type === "TIME_LIMIT") {
718
+ quota.monthlyPercent = percent;
719
+ if (resetAt !== undefined) quota.monthlyResetAt = resetAt;
720
+ windows += 1;
721
+ }
688
722
  }
689
- const body = asRecord(await readQuotaJson(response));
690
- if (!body || body.success === false) return null;
691
- const data = asRecord(body.data) ?? body;
692
- // The plugin renders a 5h token window, a weekly window, and a monthly MCP
693
- // window. Look for percent fields with window identifiers.
723
+ return windows > 0 ? quota : null;
724
+ }
725
+
726
+ /**
727
+ * Legacy Z.AI payload shape: percent fields with window identifiers directly on
728
+ * the data object (optionally nested under `quota`). Kept as a fallback so
729
+ * older responses keep rendering when the `limits` array is absent.
730
+ */
731
+ function parseZaiQuotaLegacyFields(data: Record<string, unknown> | null): ProviderQuota | null {
732
+ if (!data) return null;
694
733
  const quota: ProviderQuota = { updatedAt: Date.now() };
695
734
  let windows = 0;
696
735
  const percentAt = (key: string): number | undefined => {
697
- const value = normalizePercent(data?.[key]);
736
+ const value = normalizePercent(data[key]);
698
737
  if (value !== undefined) return value;
699
- const nested = asRecord(data?.quota);
738
+ const nested = asRecord(data.quota);
700
739
  return nested ? normalizePercent(nested[key]) : undefined;
701
740
  };
702
741
  const fiveHour = percentAt("fiveHourPercent") ?? percentAt("fiveHourUsage") ?? percentAt("fiveHourUsed");
@@ -714,7 +753,40 @@ async function fetchZaiQuota(provider: string, config: OcxProviderConfig): Promi
714
753
  quota.monthlyPercent = monthly;
715
754
  windows += 1;
716
755
  }
717
- return windows > 0 ? report(provider, "zai:quota-limit", quota) : null;
756
+ return windows > 0 ? quota : null;
757
+ }
758
+
759
+ /**
760
+ * Fetches the Z.AI GLM Coding Plan quota — on whichever region the provider
761
+ * points at (api.z.ai or open.bigmodel.cn). Authenticates with the API key as
762
+ * a Bearer token per Z.AI's API reference. The `limits` array shape is
763
+ * preferred; older field-name payloads fall back to the legacy parser.
764
+ */
765
+ async function fetchZaiQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
766
+ if (!isCanonicalZaiBaseUrl(config.baseUrl)) return null;
767
+ const apiKey = resolveEnvValue(config.apiKey)?.trim();
768
+ if (!apiKey) return null;
769
+ const normalized = normalizedBaseUrl(config.baseUrl);
770
+ const monitorHost = normalized === ZAI_BASE_URL || normalized === `${ZAI_BASE_URL}/api/coding/paas/v4`
771
+ ? ZAI_BASE_URL
772
+ : ZAI_CN_BASE_URL;
773
+ const response = await fetch(`${monitorHost}/api/monitor/usage/quota/limit`, {
774
+ headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
775
+ redirect: "error",
776
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
777
+ });
778
+ if (!response.ok) {
779
+ return response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429
780
+ ? TERMINAL_QUOTA_FAILURE
781
+ : null;
782
+ }
783
+ const body = asRecord(await readQuotaJson(response));
784
+ if (!body || body.success === false) return null;
785
+ const data = asRecord(body.data) ?? body;
786
+ const quota = Array.isArray(data?.limits)
787
+ ? parseZaiQuotaLimits(data)
788
+ : parseZaiQuotaLegacyFields(data);
789
+ return quota ? report(provider, "zai:quota-limit", quota) : null;
718
790
  }
719
791
 
720
792
  /**
@@ -2106,7 +2178,8 @@ async function maybeFetchProviderQuota(
2106
2178
  if ((provider.authMode ?? "key") === "key" && name === "cline-pass") {
2107
2179
  return fetchClineQuota(name, provider);
2108
2180
  }
2109
- if ((provider.authMode ?? "key") === "key" && name === "zai") {
2181
+ if ((provider.authMode ?? "key") === "key"
2182
+ && (name === "zai" || name === "glm" || name === "glm-cn" || name === "zhipu-bigmodel-coding")) {
2110
2183
  return fetchZaiQuota(name, provider);
2111
2184
  }
2112
2185
  if ((provider.authMode ?? "key") === "key" && (name === "minimax" || name === "minimax-cn")) {
@@ -10,6 +10,7 @@ import {
10
10
  MOONSHOT_BASE_URL_CHOICES, MOONSHOT_INTL_BASE_URL,
11
11
  } from "./base-url-choices";
12
12
  import {
13
+ CURSOR_NO_VISION_MODELS,
13
14
  CURSOR_STATIC_MODELS,
14
15
  cursorModelContextWindows,
15
16
  cursorModelIds,
@@ -224,8 +225,22 @@ export interface ProviderRegistryEntry {
224
225
  supportsServiceTier?: boolean;
225
226
  /** Registry default for OpenAI extended hosted web_search field support. */
226
227
  supportsOpenAiWebSearchToolFields?: boolean;
228
+ /** Registry default for native Responses custom-tool support. */
229
+ supportsResponsesCustomTools?: boolean;
227
230
  /** Registry default for exact model service-tier capability; explicit config keys win. */
228
231
  modelSupportsServiceTier?: Record<string, boolean>;
232
+ /**
233
+ * Registry-only service-tier defaults for an OAuth preset's explicit API-key transport.
234
+ * Applied only when `allowKeyAuthOverride` is true and the captured effective auth transport
235
+ * is key-based. Explicit provider config still wins field-by-field, including `false`.
236
+ */
237
+ keyAuthServiceTier?: {
238
+ supportsServiceTier?: boolean;
239
+ modelSupportsServiceTier?: Record<string, boolean>;
240
+ chatServiceTier?: boolean;
241
+ };
242
+ /** Provider-specific copy for the Codex catalog's Fast tier. */
243
+ fastTierDescription?: string;
229
244
  /**
230
245
  * Registry-only destination guard for `modelSupportsServiceTier`. This scopes vendor evidence
231
246
  * without changing provider ownership, routing, authentication, or config validation.
@@ -452,7 +467,23 @@ const THINKING_BUDGET_MODELS = [
452
467
  ];
453
468
  const OPENCODE_GO_THINKING_BUDGET_MODELS = ["qwen3.5-plus", "qwen3.6-plus", "qwen3.7-max", "qwen3.7-plus"];
454
469
  const DEEPSEEK_THINKING_MODELS = ["deepseek-v4-pro", "deepseek-v4-flash"];
470
+ /*
471
+ * DeepSeek's experimental vision preview (released 2026-08-21, api-docs.deepseek.com):
472
+ * text+image input on the V4 Flash base. DeepSeek positions it as a preview id;
473
+ * the expectation is that vision merges into `deepseek-v4-flash` proper later,
474
+ * at which point this id retires the same way deepseek-chat/reasoner did.
475
+ */
476
+ const DEEPSEEK_VISION_PREVIEW_MODEL = "deepseek-v4-flash-vision-exp";
455
477
  const OPENCODE_FREE_DEEPSEEK_MODELS = ["deepseek-v4-flash-free"];
478
+ /*
479
+ * OpenCode Zen's free slug for the OpenRouter stealth model "Ox Alpha"
480
+ * (openrouter.ai/stealth/ox-alpha): 1,048,576-token context, multimodal
481
+ * (text+image+video upstream; Zen serves text+image), mandatory reasoning,
482
+ * free during the stealth window. Zen displays it as "Ox Alpha Free" under
483
+ * this exact id (opencode.ai/docs/zen, verified 2026-08-21).
484
+ */
485
+ const OPENCODE_OX_ALPHA_FREE_MODEL = "x-preview-f-free";
486
+ const OX_ALPHA_CONTEXT_WINDOW = 1_048_576;
456
487
  /*
457
488
  * Zen free models that reject `image_url` upstream (#1043, and the reproducible
458
489
  * half of #1024).
@@ -994,11 +1025,10 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
994
1025
  // no-effort fallback to `kimi-k3-max` would never be reached. Mirrors the other K3
995
1026
  // routes (kimi, kimi-code, opencode-go).
996
1027
  modelDefaultReasoningEfforts: { "kimi-k3": "max" },
997
- // Cursor's wire protocol never forwards image parts (request-builder emits an unsupported-
998
- // content marker), so the vision sidecar covers ALL cursor models regardless of what the
999
- // upstream model could natively do. Live-discovered models outside the static list fall back
1000
- // to the same marker until they appear here.
1001
- noVisionModels: cursorModelIds(CURSOR_STATIC_MODELS),
1028
+ // Blind Cursor models (Auto routers, Composer, GLM-5.2, GLM-5.3) go through the vision sidecar;
1029
+ // multimodal hosts (Claude/Gemini/GPT/Kimi/Grok) take native SelectedImage. The catalog
1030
+ // still advertises image for noVision members so Codex can attach (sidecar option B).
1031
+ noVisionModels: [...CURSOR_NO_VISION_MODELS],
1002
1032
  },
1003
1033
  {
1004
1034
  id: "xai",
@@ -1007,10 +1037,21 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1007
1037
  baseUrl: "https://api.x.ai/v1",
1008
1038
  authKind: "oauth",
1009
1039
  allowKeyAuthOverride: true,
1040
+ // Priority Processing is documented for xAI's public API-key Chat Completions and
1041
+ // Responses endpoints. OAuth is a separate Grok CLI subscription gateway and remains
1042
+ // unclassified; do not turn this into a provider-wide supportsServiceTier declaration.
1043
+ keyAuthServiceTier: {
1044
+ supportsServiceTier: true,
1045
+ chatServiceTier: true,
1046
+ },
1047
+ fastTierDescription: "Priority processing, 2x token price",
1010
1048
  featured: true,
1011
1049
  oauthId: "xai",
1012
1050
  jawcodeBundle: "xai",
1013
1051
  supportsOpenAiWebSearchToolFields: false,
1052
+ // Live A/B on 2026-08-20: xAI rejects native custom/custom_tool_call shapes while accepting
1053
+ // the otherwise-identical request after the custom tool is lowered to a function.
1054
+ supportsResponsesCustomTools: false,
1014
1055
  note: "Log in with your Grok account",
1015
1056
  // Parallel tool calls: officially supported and default-on per docs.x.ai function-calling
1016
1057
  // (verified 260709, devlog/_plan/260709_parallel_tool_calls). Streamed calls arrive whole
@@ -1101,6 +1142,17 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1101
1142
  // Unknown/new live models deliberately do not advertise a reasoning picker.
1102
1143
  reasoningEfforts: [],
1103
1144
  modelReasoningEfforts: COMMAND_CODE_MODEL_REASONING_EFFORTS,
1145
+ // Ox Alpha (stealth preview, changelog v1.31.0): free 1M multimodal reasoning
1146
+ // model on every plan. DeepSeek vision preview id is preemptive metadata —
1147
+ // it is expected to merge into deepseek-v4-flash later.
1148
+ modelContextWindows: {
1149
+ "stealth/ox-alpha": OX_ALPHA_CONTEXT_WINDOW,
1150
+ [`deepseek/${DEEPSEEK_VISION_PREVIEW_MODEL}`]: 1_048_576,
1151
+ },
1152
+ modelInputModalities: {
1153
+ "stealth/ox-alpha": ["text", "image"],
1154
+ [`deepseek/${DEEPSEEK_VISION_PREVIEW_MODEL}`]: ["text", "image"],
1155
+ },
1104
1156
  defaultMaxOutputTokens: 64_000,
1105
1157
  // The proprietary generate wire has no verified per-request serialization flag.
1106
1158
  parallelToolCalls: false,
@@ -1292,8 +1344,20 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1292
1344
  - 장점, 단점 및 영향: Luna reaches `/responses` from every inbound surface without changing siblings; a future upstream endpoint change requires an evidence-backed registry update.
1293
1345
  */
1294
1346
  modelWireDefaults: { "gpt-5.6-luna": "openai-responses" },
1295
- modelContextWindows: { "kimi-k3": KIMI_K3_STANDARD_CONTEXT_WINDOW },
1296
- modelInputModalities: { "kimi-k3": ["text", "image"] },
1347
+ modelContextWindows: {
1348
+ "kimi-k3": KIMI_K3_STANDARD_CONTEXT_WINDOW,
1349
+ // Ox Alpha (stealth 1M multimodal) and the DeepSeek vision preview are
1350
+ // metadata-only here: the Go roster is discovered live, so these apply
1351
+ // the moment the gateway starts serving the ids.
1352
+ [OPENCODE_OX_ALPHA_FREE_MODEL]: OX_ALPHA_CONTEXT_WINDOW,
1353
+ [DEEPSEEK_VISION_PREVIEW_MODEL]: 1_048_576,
1354
+ },
1355
+ modelInputModalities: {
1356
+ "kimi-k3": ["text", "image"],
1357
+ [OPENCODE_OX_ALPHA_FREE_MODEL]: ["text", "image"],
1358
+ // Experimental DeepSeek vision preview — expected to merge into deepseek-v4-flash later.
1359
+ [DEEPSEEK_VISION_PREVIEW_MODEL]: ["text", "image"],
1360
+ },
1297
1361
  modelReasoningEfforts: {
1298
1362
  "glm-5.3": ZAI_GLM_53_REASONING_EFFORTS,
1299
1363
  "glm-5.2": ZAI_GLM_52_REASONING_EFFORTS,
@@ -1396,11 +1460,16 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1396
1460
  featured: true,
1397
1461
  dashboardUrl: "https://openrouter.ai/keys",
1398
1462
  jawcodeBundle: "openrouter",
1399
- models: ["anthropic/claude-sonnet-5", ...OPENROUTER_GPT56_MODELS],
1463
+ // stealth/ox-alpha: free stealth-window frontier model (launched 2026-08-20).
1464
+ // /api/v1/models reports 1,048,576 context, 131,072 max output, text+image+video
1465
+ // input, $0 pricing, mandatory reasoning. Single provider slug: `stealth`.
1466
+ models: ["anthropic/claude-sonnet-5", "stealth/ox-alpha", ...OPENROUTER_GPT56_MODELS],
1400
1467
  modelContextWindows: {
1401
1468
  "anthropic/claude-sonnet-5": 1_000_000,
1469
+ "stealth/ox-alpha": OX_ALPHA_CONTEXT_WINDOW,
1402
1470
  ...OPENROUTER_GPT56_CONTEXT_WINDOWS,
1403
1471
  },
1472
+ modelInputModalities: { "stealth/ox-alpha": ["text", "image"] },
1404
1473
  // OpenRouter documents priority support for OpenAI endpoints, but not Anthropic. Keep the
1405
1474
  // provider unclassified and opt in only the exact OpenAI-backed slugs we ship. These facts
1406
1475
  // belong only to the canonical destination; a same-named custom gateway is unknown to us.
@@ -1551,11 +1620,14 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1551
1620
  // keep validating and routing (they previously mapped to v4-flash; devlog
1552
1621
  // _fin/260710_provider_hardening/002_research_cn.md). The current offerings are
1553
1622
  // the V4 ids — defaultModel and the model-specific wiring above use them.
1554
- models: ["deepseek-chat", "deepseek-reasoner", ...DEEPSEEK_THINKING_MODELS],
1623
+ // deepseek-v4-flash-vision-exp: experimental vision preview (2026-08-21)
1624
+ // expected to merge into deepseek-v4-flash later; see DEEPSEEK_VISION_PREVIEW_MODEL.
1625
+ models: ["deepseek-chat", "deepseek-reasoner", ...DEEPSEEK_THINKING_MODELS, DEEPSEEK_VISION_PREVIEW_MODEL],
1555
1626
  defaultModel: "deepseek-v4-flash",
1556
1627
  // Official DeepSeek Codex setup (codex-deepseek-setup.sh) advertises 1,048,576
1557
1628
  // for both V4 models; the older 1,000,000 figure was a rounded approximation.
1558
- modelContextWindows: { "deepseek-v4-flash": 1_048_576, "deepseek-v4-pro": 1_048_576 },
1629
+ modelContextWindows: { "deepseek-v4-flash": 1_048_576, "deepseek-v4-pro": 1_048_576, [DEEPSEEK_VISION_PREVIEW_MODEL]: 1_048_576 },
1630
+ modelInputModalities: { [DEEPSEEK_VISION_PREVIEW_MODEL]: ["text", "image"] },
1559
1631
  // DeepSeek documents both V4 models as native Responses API models adapted for Codex
1560
1632
  // (model table marks Responses API ✓ for flash and pro; the /responses reference lists
1561
1633
  // both ids as accepted `model` values — verified 2026-08-13 with the V4 Pro GA,
@@ -1808,6 +1880,17 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
1808
1880
  // slash ids — so a Codex-facing slug like `commandcode/deepseek-deepseek-v4-pro`
1809
1881
  // is sent upstream verbatim and rejected with `unsupported_model`.
1810
1882
  modelReasoningEfforts: COMMAND_CODE_MODEL_REASONING_EFFORTS,
1883
+ // Ox Alpha (stealth preview, Command Code changelog v1.31.0) ships with a
1884
+ // 1.05M-token multimodal context; the DeepSeek vision preview id is
1885
+ // preemptive for when the catalog serves it (merges into v4-flash later).
1886
+ modelContextWindows: {
1887
+ "stealth/ox-alpha": OX_ALPHA_CONTEXT_WINDOW,
1888
+ [`deepseek/${DEEPSEEK_VISION_PREVIEW_MODEL}`]: 1_048_576,
1889
+ },
1890
+ modelInputModalities: {
1891
+ "stealth/ox-alpha": ["text", "image"],
1892
+ [`deepseek/${DEEPSEEK_VISION_PREVIEW_MODEL}`]: ["text", "image"],
1893
+ },
1811
1894
  modelDiscovery: {
1812
1895
  path: "models",
1813
1896
  maxResponseBytes: 256 * 1024,
@@ -2452,6 +2535,16 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
2452
2535
  [...DEEPSEEK_THINKING_MODELS, ...OPENCODE_FREE_DEEPSEEK_MODELS].map(id => [id, deepseekReasoningMapFor(id)]),
2453
2536
  ),
2454
2537
  preserveReasoningContentModels: [...DEEPSEEK_THINKING_MODELS, ...OPENCODE_FREE_DEEPSEEK_MODELS],
2538
+ // Same Zen gateway as opencode-free: Ox Alpha Free (1M multimodal stealth model)
2539
+ // and the DeepSeek vision preview (merges into deepseek-v4-flash later).
2540
+ modelContextWindows: {
2541
+ [OPENCODE_OX_ALPHA_FREE_MODEL]: OX_ALPHA_CONTEXT_WINDOW,
2542
+ [DEEPSEEK_VISION_PREVIEW_MODEL]: 1_048_576,
2543
+ },
2544
+ modelInputModalities: {
2545
+ [OPENCODE_OX_ALPHA_FREE_MODEL]: ["text", "image"],
2546
+ [DEEPSEEK_VISION_PREVIEW_MODEL]: ["text", "image"],
2547
+ },
2455
2548
  noVisionModels: [...OPENCODE_ZEN_TEXT_ONLY_MODELS, ...DEEPSEEK_THINKING_MODELS],
2456
2549
  },
2457
2550
  { id: "vercel-ai-gateway", label: "Vercel AI Gateway", baseUrl: "https://ai-gateway.vercel.sh/v1", adapter: "openai-chat", authKind: "key", dashboardUrl: "https://vercel.com/dashboard" },
@@ -2482,6 +2575,18 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
2482
2575
  modelReasoningEfforts: Object.fromEntries(OPENCODE_FREE_DEEPSEEK_MODELS.map(id => [id, deepseekThinkingEffortsFor(id)])),
2483
2576
  modelReasoningEffortMap: Object.fromEntries(OPENCODE_FREE_DEEPSEEK_MODELS.map(id => [id, deepseekReasoningMapFor(id)])),
2484
2577
  preserveReasoningContentModels: OPENCODE_FREE_DEEPSEEK_MODELS,
2578
+ // Ox Alpha Free (`x-preview-f-free`): the OpenRouter stealth model on Zen's
2579
+ // free tier — 1,048,576 context, text+image input. Deliberately NOT in the
2580
+ // text-only list below. The DeepSeek vision preview id is preemptive
2581
+ // metadata for when Zen starts serving it (merges into v4-flash later).
2582
+ modelContextWindows: {
2583
+ [OPENCODE_OX_ALPHA_FREE_MODEL]: OX_ALPHA_CONTEXT_WINDOW,
2584
+ [DEEPSEEK_VISION_PREVIEW_MODEL]: 1_048_576,
2585
+ },
2586
+ modelInputModalities: {
2587
+ [OPENCODE_OX_ALPHA_FREE_MODEL]: ["text", "image"],
2588
+ [DEEPSEEK_VISION_PREVIEW_MODEL]: ["text", "image"],
2589
+ },
2485
2590
  // Same Zen roster behind the same base URL, so it carries the same measured
2486
2591
  // text-only list rather than only its DeepSeek member (#1043).
2487
2592
  noVisionModels: OPENCODE_ZEN_TEXT_ONLY_MODELS,
@@ -70,11 +70,22 @@ function buildFastPolicyAuthority(
70
70
  capabilityProvider: ServiceTierCapabilityProvider = provider,
71
71
  ): FastPolicyAuthority {
72
72
  const registry = registryTransportMatch ? getProviderRegistryEntry(providerName) : undefined;
73
+ const authTransport = resolveProviderAuthTransport(
74
+ provider.adapter,
75
+ provider.authMode ?? registry?.authKind ?? "key",
76
+ provider.apiKeyTransport,
77
+ );
78
+ const keyAuthDefaults = registry?.allowKeyAuthOverride === true
79
+ && (authTransport === "authorization_bearer" || authTransport === "x_api_key")
80
+ ? registry.keyAuthServiceTier
81
+ : undefined;
73
82
  const registryModelCapabilities = registry
74
83
  && registryModelServiceTierCapabilityApplies(registry, capabilityProvider)
75
84
  ? registry.modelSupportsServiceTier
76
85
  : undefined;
77
- const providerCapability = capabilityProvider.supportsServiceTier ?? registry?.supportsServiceTier;
86
+ const providerCapability = capabilityProvider.supportsServiceTier
87
+ ?? keyAuthDefaults?.supportsServiceTier
88
+ ?? registry?.supportsServiceTier;
78
89
  const authority: FastPolicyAuthority = Object.freeze({
79
90
  providerAdapter: provider.adapter,
80
91
  providerAuthMode: provider.authMode ?? registry?.authKind ?? "key",
@@ -82,19 +93,23 @@ function buildFastPolicyAuthority(
82
93
  provider.fastWire !== undefined ? provider.fastWire : registry?.fastWire,
83
94
  { freeze: true },
84
95
  ),
96
+ ...(registry?.fastTierDescription !== undefined
97
+ ? { fastTierDescription: registry.fastTierDescription }
98
+ : {}),
85
99
  modelWireOverrideAllowed: !isCanonicalOpenAiForwardProvider(provider as OcxProviderConfig),
86
- authTransport: resolveProviderAuthTransport(
87
- provider.adapter,
88
- provider.authMode ?? registry?.authKind ?? "key",
89
- provider.apiKeyTransport,
90
- ),
100
+ authTransport,
91
101
  capability: Object.freeze({
92
102
  ...(providerCapability !== undefined ? { provider: providerCapability } : {}),
93
103
  models: Object.freeze({
94
104
  ...(registryModelCapabilities ?? {}),
105
+ ...(keyAuthDefaults?.modelSupportsServiceTier ?? {}),
95
106
  ...(capabilityProvider.modelSupportsServiceTier ?? {}),
96
107
  }),
97
- ...(provider.chatServiceTier !== undefined ? { chatServiceTier: provider.chatServiceTier } : {}),
108
+ ...(provider.chatServiceTier !== undefined
109
+ ? { chatServiceTier: provider.chatServiceTier }
110
+ : keyAuthDefaults?.chatServiceTier !== undefined
111
+ ? { chatServiceTier: keyAuthDefaults.chatServiceTier }
112
+ : {}),
98
113
  }),
99
114
  modelAdapters: Object.freeze({ ...(provider.modelAdapters ?? {}) }),
100
115
  hardPins: captureWireAdapterHardPins(providerName),
@@ -4,6 +4,13 @@ import { collectResponsesToolGroups } from "./tool-groups";
4
4
  const ROUTED_CUSTOM_TOOL_PASSTHROUGH = new Set(["apply_patch"]);
5
5
  const BUILTIN_FUNCTIONS_NAMESPACE = "functions";
6
6
 
7
+ function routedCustomToolPassesThrough(
8
+ name: string,
9
+ supportsResponsesCustomTools: boolean | undefined,
10
+ ): boolean {
11
+ return supportsResponsesCustomTools !== false && ROUTED_CUSTOM_TOOL_PASSTHROUGH.has(name);
12
+ }
13
+
7
14
  function isPlainObject(value: unknown): value is Record<string, unknown> {
8
15
  return !!value && typeof value === "object" && !Array.isArray(value);
9
16
  }
@@ -34,7 +41,10 @@ export function routedCustomToolWireName(value: unknown): string | undefined {
34
41
  * Names of converted custom declarations after namespace lowering. Restoration uses these exact
35
42
  * wire identities so same-named function and custom children in different namespaces stay distinct.
36
43
  */
37
- function collectRoutedCustomToolWireNames(body: unknown): Set<string> {
44
+ function collectRoutedCustomToolWireNames(
45
+ body: unknown,
46
+ supportsResponsesCustomTools?: boolean,
47
+ ): Set<string> {
38
48
  const names = new Set<string>();
39
49
  const groups = collectResponsesToolGroups(body);
40
50
  const bareWireNames = new Set<string>();
@@ -54,7 +64,7 @@ function collectRoutedCustomToolWireNames(body: unknown): Set<string> {
54
64
  if (
55
65
  tool.type === "custom"
56
66
  && typeof tool.name === "string"
57
- && !ROUTED_CUSTOM_TOOL_PASSTHROUGH.has(tool.name)
67
+ && !routedCustomToolPassesThrough(tool.name, supportsResponsesCustomTools)
58
68
  ) {
59
69
  names.add(tool.name);
60
70
  continue;
@@ -67,7 +77,7 @@ function collectRoutedCustomToolWireNames(body: unknown): Set<string> {
67
77
  isPlainObject(child)
68
78
  && child.type === "custom"
69
79
  && typeof child.name === "string"
70
- && !ROUTED_CUSTOM_TOOL_PASSTHROUGH.has(child.name)
80
+ && !routedCustomToolPassesThrough(child.name, supportsResponsesCustomTools)
71
81
  && !(tool.name === BUILTIN_FUNCTIONS_NAMESPACE && bareWireNames.has(child.name))
72
82
  ) names.add(customToolWireName(tool.name, child.name));
73
83
  }
@@ -81,7 +91,10 @@ export function customToolItemId(id: unknown): unknown {
81
91
  return id.startsWith("fc_") ? `ctc_${id.slice(3)}` : id;
82
92
  }
83
93
 
84
- export function collectRoutedCustomToolNames(body: unknown): Set<string> {
94
+ export function collectRoutedCustomToolNames(
95
+ body: unknown,
96
+ supportsResponsesCustomTools?: boolean,
97
+ ): Set<string> {
85
98
  const names = new Set<string>();
86
99
  const visit = (value: unknown): void => {
87
100
  if (Array.isArray(value)) {
@@ -92,7 +105,7 @@ export function collectRoutedCustomToolNames(body: unknown): Set<string> {
92
105
  if (
93
106
  value.type === "custom"
94
107
  && typeof value.name === "string"
95
- && !ROUTED_CUSTOM_TOOL_PASSTHROUGH.has(value.name)
108
+ && !routedCustomToolPassesThrough(value.name, supportsResponsesCustomTools)
96
109
  ) {
97
110
  names.add(value.name);
98
111
  }
@@ -184,12 +197,15 @@ function rewriteForUpstream(
184
197
  return changed ? next : value;
185
198
  }
186
199
 
187
- export function rewriteRoutedCustomToolsForUpstream(body: unknown): {
200
+ export function rewriteRoutedCustomToolsForUpstream(
201
+ body: unknown,
202
+ supportsResponsesCustomTools?: boolean,
203
+ ): {
188
204
  body: unknown;
189
205
  names: Set<string>;
190
206
  } {
191
- const conversionNames = collectRoutedCustomToolNames(body);
192
- const names = collectRoutedCustomToolWireNames(body);
207
+ const conversionNames = collectRoutedCustomToolNames(body, supportsResponsesCustomTools);
208
+ const names = collectRoutedCustomToolWireNames(body, supportsResponsesCustomTools);
193
209
  if (conversionNames.size === 0) return { body, names };
194
210
  const callIds = new Set<string>();
195
211
  collectConvertedCallIds(body, conversionNames, callIds);