@bitkyc08/opencodex 2.11.0 → 2.11.1

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 (92) hide show
  1. package/gui/dist/assets/{index-Bk-PN-70.css → index-BA1dgo4q.css} +1 -1
  2. package/gui/dist/assets/index-DDZpgzKk.js +70 -0
  3. package/gui/dist/index.html +2 -2
  4. package/package.json +6 -4
  5. package/src/adapters/anthropic.ts +10 -2
  6. package/src/adapters/cursor/native-exec-common.ts +6 -2
  7. package/src/adapters/google-antigravity-replay.ts +3 -2
  8. package/src/adapters/google-antigravity-wire.ts +38 -6
  9. package/src/adapters/google.ts +57 -10
  10. package/src/adapters/openai-chat.ts +19 -2
  11. package/src/claude/model-info.ts +1 -1
  12. package/src/cli/account-auth.ts +3 -1
  13. package/src/cli/account-catalog-refresh.ts +14 -0
  14. package/src/cli/account-extended.ts +15 -1
  15. package/src/cli/codex-shim-readiness.ts +69 -0
  16. package/src/cli/combo.ts +5 -0
  17. package/src/cli/config-command.ts +33 -4
  18. package/src/cli/doctor.ts +21 -0
  19. package/src/cli/export-command.ts +6 -6
  20. package/src/cli/help.ts +6 -6
  21. package/src/cli/index.ts +22 -5
  22. package/src/cli/models-runtime.ts +13 -1
  23. package/src/cli/provider.ts +7 -0
  24. package/src/clients/config-export.ts +67 -5
  25. package/src/codex/account-lifecycle.ts +99 -10
  26. package/src/codex/auth-api.ts +222 -34
  27. package/src/codex/catalog/account-models.ts +9 -4
  28. package/src/codex/catalog/aggregation.ts +41 -8
  29. package/src/codex/catalog/bundled.ts +54 -22
  30. package/src/codex/catalog/effort.ts +47 -20
  31. package/src/codex/catalog/kinds.ts +2 -0
  32. package/src/codex/catalog/metadata.ts +70 -18
  33. package/src/codex/catalog/native-models.ts +7 -0
  34. package/src/codex/catalog/parsing.ts +29 -11
  35. package/src/codex/catalog/provider-fetch.ts +335 -50
  36. package/src/codex/catalog/sync.ts +588 -126
  37. package/src/codex/catalog-refresh-status.ts +87 -0
  38. package/src/codex/catalog-write-serialization.ts +2 -1
  39. package/src/codex/catalog.ts +4 -3
  40. package/src/codex/convergence-types.ts +1 -1
  41. package/src/codex/convergence.ts +190 -52
  42. package/src/codex/custom-model-catalog-migration.ts +176 -0
  43. package/src/codex/features.ts +79 -1
  44. package/src/codex/history-job.ts +81 -1
  45. package/src/codex/history-lock.ts +2 -1
  46. package/src/codex/history-provider.ts +4 -3
  47. package/src/codex/inject.ts +56 -12
  48. package/src/codex/model-cache.ts +50 -10
  49. package/src/codex/transition-state.ts +10 -2
  50. package/src/codex/user-identity.ts +110 -2
  51. package/src/combos/index.ts +3 -0
  52. package/src/combos/types.ts +75 -9
  53. package/src/config.ts +26 -12
  54. package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +12 -10
  55. package/src/integrations/registry.ts +7 -0
  56. package/src/integrations/writer.ts +1 -1
  57. package/src/lib/pinned-http.ts +40 -9
  58. package/src/lib/process-control.ts +4 -1
  59. package/src/lib/provider-outbound.ts +42 -9
  60. package/src/oauth/cursor.ts +25 -5
  61. package/src/oauth/index.ts +59 -45
  62. package/src/providers/antigravity-models.ts +115 -3
  63. package/src/providers/context-cap.ts +13 -5
  64. package/src/providers/model-discovery-limits.ts +16 -0
  65. package/src/providers/model-discovery.ts +14 -15
  66. package/src/providers/provider-id-rewrite.ts +29 -0
  67. package/src/providers/quota.ts +93 -19
  68. package/src/providers/registry.ts +16 -5
  69. package/src/providers/slug-codec.ts +13 -6
  70. package/src/reasoning-effort.ts +22 -0
  71. package/src/router.ts +0 -20
  72. package/src/routing/history/indexer.ts +54 -39
  73. package/src/routing/quota.ts +77 -56
  74. package/src/server/index.ts +32 -7
  75. package/src/server/management/combo-routes.ts +43 -19
  76. package/src/server/management/config-routes.ts +115 -20
  77. package/src/server/management/model-routes.ts +10 -7
  78. package/src/server/management/model-rows.ts +6 -2
  79. package/src/server/management/oauth-account-routes.ts +12 -0
  80. package/src/server/management/provider-routes.ts +114 -40
  81. package/src/server/management/routing-profile-routes.ts +0 -22
  82. package/src/server/management-api.ts +7 -25
  83. package/src/server/responses/core.ts +3 -3
  84. package/src/server/responses/policy-fallback.ts +152 -0
  85. package/src/server/responses.ts +3 -2
  86. package/src/types.ts +15 -2
  87. package/src/usage/cost.ts +0 -0
  88. package/src/vision/describe.ts +3 -1
  89. package/src/vision/index.ts +17 -6
  90. package/src/vision/reasoning.ts +55 -0
  91. package/src/web-search/parse.ts +15 -3
  92. package/gui/dist/assets/index-BynIEIV-.js +0 -70
@@ -4,9 +4,12 @@ export {
4
4
  comboConfigError,
5
5
  comboConfigIssues,
6
6
  comboDefaultEffort,
7
+ comboDisabledModelId,
8
+ comboDisabledModelSelectors,
7
9
  comboModelId,
8
10
  comboPublicModelId,
9
11
  getCombo,
12
+ isNativeAliasCombo,
10
13
  isValidComboId,
11
14
  listComboIds,
12
15
  normalizeComboConfig,
@@ -1,4 +1,5 @@
1
1
  import { isCodexReasoningEffort } from "../reasoning-effort";
2
+ import { SUPPORTED_NATIVE_OPENAI_SLUGS } from "../codex/catalog/native-models";
2
3
  import type {
3
4
  OcxComboConfig,
4
5
  OcxComboDefaultEffort,
@@ -24,11 +25,7 @@ const COMBO_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
24
25
  * `combo/` prefix. Codex-facing slugs tolerate at most one "/", so deeper paths reject.
25
26
  */
26
27
  const COMBO_ALIAS_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}(?:\/[A-Za-z0-9][A-Za-z0-9._-]{0,63})?$/;
27
- /**
28
- * Bare aliases in the OpenAI native family (gpt-*, o1-*, o3-*, o4-*, codex-*) are
29
- * rejected: they collide with native catalog rows and the canonical-OpenAI routing
30
- * branch, which cannot be shadowed honestly.
31
- */
28
+ /** Bare aliases in this family require the explicit `nativeAlias` opt-in below. */
32
29
  const NATIVE_OPENAI_FAMILY_PATTERN = /^(?:gpt-|o1-|o3-|o4-|codex-)/;
33
30
 
34
31
  export interface ComboValidationIssue {
@@ -42,9 +39,22 @@ export interface NormalizedComboConfig {
42
39
  defaultEffort: OcxComboDefaultEffort | null;
43
40
  /** Trimmed public alias, or null when the combo keeps the default `combo/<id>` slug. */
44
41
  alias: string | null;
42
+ /** Explicit native-family alias opt-in. */
43
+ nativeAlias: boolean;
44
+ /** Display-only label for the catalog row, or null when unset. */
45
+ displayName: string | null;
45
46
  targets: Array<Required<OcxComboTarget>>;
46
47
  }
47
48
 
49
+ /** True only for an explicitly opted-in bare native-family alias. */
50
+ export function isNativeAliasCombo(
51
+ combo: { alias?: string | null; nativeAlias?: boolean },
52
+ ): boolean {
53
+ const alias = typeof combo.alias === "string" ? combo.alias.trim() : "";
54
+ return combo.nativeAlias === true
55
+ && SUPPORTED_NATIVE_OPENAI_SLUGS.has(alias);
56
+ }
57
+
48
58
  export function targetKey(target: Pick<OcxComboTarget, "provider" | "model">): string {
49
59
  return `${target.provider}/${target.model}`;
50
60
  }
@@ -66,6 +76,27 @@ export function comboPublicModelId(id: string, combo: { alias?: string | null })
66
76
  return alias || comboModelId(id);
67
77
  }
68
78
 
79
+ /**
80
+ * Persisted selector that hides a combo from discovery. Native aliases keep the canonical
81
+ * `combo/<id>` selector because their bare public id remains the native OpenAI disable key.
82
+ */
83
+ export function comboDisabledModelId(
84
+ id: string,
85
+ combo: { alias?: string | null; nativeAlias?: boolean },
86
+ ): string {
87
+ return isNativeAliasCombo(combo) ? comboModelId(id) : comboPublicModelId(id, combo);
88
+ }
89
+
90
+ /** Every persisted selector that can refer to this combo in `disabledModels`. */
91
+ export function comboDisabledModelSelectors(
92
+ id: string,
93
+ combo: { alias?: string | null; nativeAlias?: boolean },
94
+ ): string[] {
95
+ const canonical = comboModelId(id);
96
+ const preferred = comboDisabledModelId(id, combo);
97
+ return preferred === canonical ? [canonical] : [canonical, preferred];
98
+ }
99
+
69
100
  /**
70
101
  * Resolve a client-requested model id to a combo config key. The canonical `combo/<id>`
71
102
  * form wins first (back-compat); otherwise an exact alias match across configured combos.
@@ -94,7 +125,7 @@ export function comboAliasIssues(
94
125
  id: string,
95
126
  alias: string,
96
127
  combos: Record<string, OcxComboConfig> | undefined,
97
- options: { excludeComboId?: string } = {},
128
+ options: { excludeComboId?: string; allowNativeAlias?: boolean } = {},
98
129
  ): ComboValidationIssue[] {
99
130
  const issues: ComboValidationIssue[] = [];
100
131
  if (!COMBO_ALIAS_PATTERN.test(alias)) {
@@ -110,10 +141,12 @@ export function comboAliasIssues(
110
141
  message: `alias must not use the reserved "${COMBO_NAMESPACE}/" namespace`,
111
142
  });
112
143
  }
113
- if (!alias.includes("/") && NATIVE_OPENAI_FAMILY_PATTERN.test(alias)) {
144
+ if (!alias.includes("/")
145
+ && NATIVE_OPENAI_FAMILY_PATTERN.test(alias)
146
+ && options.allowNativeAlias !== true) {
114
147
  issues.push({
115
148
  path: ["alias"],
116
- message: "bare aliases in the OpenAI native family (gpt-*, o1-*, o3-*, o4-*, codex-*) are not allowed",
149
+ message: "bare aliases in the OpenAI native family require nativeAlias=true",
117
150
  });
118
151
  }
119
152
  for (const [otherId, other] of Object.entries(combos ?? {})) {
@@ -193,9 +226,39 @@ export function comboConfigIssues(
193
226
  issues.push({ path: ["alias"], message: "alias must be a string" });
194
227
  } else {
195
228
  const alias = body.alias.trim();
196
- if (alias) issues.push(...comboAliasIssues(id, alias, options.combos, options));
229
+ if (alias) {
230
+ issues.push(...comboAliasIssues(id, alias, options.combos, {
231
+ ...options,
232
+ allowNativeAlias: body.nativeAlias === true,
233
+ }));
234
+ }
235
+ }
236
+ }
237
+
238
+ if (body.nativeAlias !== undefined && typeof body.nativeAlias !== "boolean") {
239
+ issues.push({ path: ["nativeAlias"], message: "nativeAlias must be a boolean" });
240
+ }
241
+ if (body.displayName !== undefined) {
242
+ if (typeof body.displayName !== "string") {
243
+ issues.push({ path: ["displayName"], message: "displayName must be a string" });
244
+ } else if (body.displayName.trim().length > 128 || /[\u0000-\u001f\u007f]/.test(body.displayName)) {
245
+ issues.push({
246
+ path: ["displayName"],
247
+ message: "displayName must be at most 128 characters and contain no control characters",
248
+ });
197
249
  }
198
250
  }
251
+ const alias = typeof body.alias === "string" ? body.alias.trim() : "";
252
+ const nativeAlias = body.nativeAlias === true;
253
+ if (nativeAlias && !SUPPORTED_NATIVE_OPENAI_SLUGS.has(alias)) {
254
+ issues.push({
255
+ path: ["nativeAlias"],
256
+ message: "nativeAlias requires a currently supported bare OpenAI-native model alias",
257
+ });
258
+ }
259
+ if (nativeAlias && (typeof body.displayName !== "string" || body.displayName.trim().length === 0)) {
260
+ issues.push({ path: ["displayName"], message: "displayName is required for native aliases" });
261
+ }
199
262
 
200
263
  if (!Array.isArray(body.targets) || body.targets.length === 0) {
201
264
  issues.push({ path: ["targets"], message: "targets must be a non-empty array" });
@@ -271,11 +334,14 @@ export function comboConfigError(
271
334
 
272
335
  export function normalizeComboConfig(raw: OcxComboConfig): NormalizedComboConfig {
273
336
  const alias = typeof raw.alias === "string" ? raw.alias.trim() : "";
337
+ const displayName = typeof raw.displayName === "string" ? raw.displayName.trim() : "";
274
338
  return {
275
339
  strategy: raw.strategy ?? "failover",
276
340
  stickyLimit: raw.stickyLimit ?? 1,
277
341
  defaultEffort: raw.defaultEffort ?? null,
278
342
  alias: alias || null,
343
+ nativeAlias: raw.nativeAlias === true,
344
+ displayName: displayName || null,
279
345
  targets: raw.targets.map(target => ({
280
346
  provider: target.provider.trim(),
281
347
  model: target.model.trim(),
package/src/config.ts CHANGED
@@ -29,6 +29,10 @@ import {
29
29
  } from "./codex/account-namespace-match";
30
30
  import { isCodexAccountPriorityKey } from "./codex/account-priority";
31
31
  import { UPSTREAM_HOST_CIRCUIT_MAX_THRESHOLD } from "./codex/upstream-host-health";
32
+ import {
33
+ adoptCustomModelCatalogMigration,
34
+ projectCustomModelCatalogMigration,
35
+ } from "./codex/custom-model-catalog-migration";
32
36
  import { parseAccountPriority } from "./codex/pool-rotation";
33
37
  import { COMBO_NAMESPACE, comboConfigIssues } from "./combos/types";
34
38
  import { routingProfileIssues } from "./routing/profile";
@@ -1053,9 +1057,8 @@ const configSchema = z.object({
1053
1057
  providers: z.record(z.string(), providerConfigSchema),
1054
1058
  defaultProvider: z.string().min(1).default("openai"),
1055
1059
  openaiProviderTierVersion: z.union([z.literal(1), z.literal(2)]).optional(),
1056
- // Invalid hand edits must not discard an otherwise usable config. Treat them as
1057
- // pre-migration so startup can safely re-run the one-time normalization.
1058
- googleAntigravityStaticCatalogVersion: z.literal(1).optional().catch(undefined),
1060
+ // Invalid hand edits must not discard an otherwise usable config.
1061
+ googleAntigravityStaticCatalogVersion: z.union([z.literal(1), z.literal(2)]).optional().catch(undefined),
1059
1062
  clientIntegrations: clientIntegrationsSchema.optional().catch(undefined),
1060
1063
  providerContextCaps: z.record(z.string(), z.number().int().positive()).optional(),
1061
1064
  contextCapValue: z.number().int().positive().optional(),
@@ -2000,8 +2003,8 @@ function googleAntigravityStaticCatalogVersionError(value: unknown): string | nu
2000
2003
  const raw = rawConfigRecord(value);
2001
2004
  if (!raw || !Object.hasOwn(raw, "googleAntigravityStaticCatalogVersion")) return null;
2002
2005
  const version = raw.googleAntigravityStaticCatalogVersion;
2003
- if (version === undefined || version === 1) return null;
2004
- return "schema_invalid: googleAntigravityStaticCatalogVersion: must be 1 or omitted";
2006
+ if (version === undefined || version === 1 || version === 2) return null;
2007
+ return "schema_invalid: googleAntigravityStaticCatalogVersion: must be 1, 2, or omitted";
2005
2008
  }
2006
2009
 
2007
2010
  function codexAccountPickerEnabledError(value: unknown): string | null {
@@ -2381,7 +2384,9 @@ export function saveConfig(config: OcxConfig): void {
2381
2384
  // Keep the real-home assertion ahead of even lock-directory preparation.
2382
2385
  assertNotRealHomeUnderTest(getConfigDir());
2383
2386
  withConfigMutationLockSync(() => {
2384
- if (persistConfigUnlocked(config)) bumpGenerationForCooperatingConfigWrite();
2387
+ const projected = projectCustomModelCatalogMigration(readRawConfigJson(), config);
2388
+ if (persistConfigUnlocked(projected)) bumpGenerationForCooperatingConfigWrite();
2389
+ adoptCustomModelCatalogMigration(config, projected);
2385
2390
  });
2386
2391
  }
2387
2392
 
@@ -2462,7 +2467,11 @@ export function mutatePersistedConfig<T>(
2462
2467
  continue;
2463
2468
  }
2464
2469
 
2465
- if (persistConfigUnlocked(confirmedConfig)) bumpGenerationForCooperatingConfigWrite();
2470
+ const projected = projectCustomModelCatalogMigration(
2471
+ commitBase.diagnostics.config,
2472
+ confirmedConfig,
2473
+ );
2474
+ if (persistConfigUnlocked(projected)) bumpGenerationForCooperatingConfigWrite();
2466
2475
  return { status: "committed", value: confirmed.value };
2467
2476
  }
2468
2477
  return { status: "unavailable", reason: "conflict" };
@@ -2700,9 +2709,9 @@ function readPersistedServerBinding(
2700
2709
  export function saveConfigPreservingClaudeCode(config: OcxConfig): void {
2701
2710
  withConfigMutationLockSync(() => {
2702
2711
  const bindingBaseline = persistedLiveServerBinding.get(config);
2703
- const onDisk = claudeCodeBaseline.has(config) || bindingBaseline
2704
- ? readRawConfigJson()
2705
- : undefined;
2712
+ // One authoritative pre-write read feeds both the live-config reconciliation and
2713
+ // custom-model deletion migration. A second read could observe different bytes.
2714
+ const onDisk = readRawConfigJson();
2706
2715
  if (claudeCodeBaseline.has(config)) {
2707
2716
  if (onDisk !== undefined) {
2708
2717
  const baseline = claudeCodeBaseline.get(config);
@@ -2714,18 +2723,23 @@ export function saveConfigPreservingClaudeCode(config: OcxConfig): void {
2714
2723
  }
2715
2724
  }
2716
2725
  }
2726
+ const projectedConfig = projectCustomModelCatalogMigration(
2727
+ onDisk,
2728
+ config,
2729
+ );
2717
2730
  const persistedBinding = bindingBaseline && onDisk
2718
2731
  ? readPersistedServerBinding(onDisk, bindingBaseline)
2719
2732
  : bindingBaseline;
2720
2733
  if (persistedBinding) {
2721
- const persistedConfig: OcxConfig = { ...config, port: persistedBinding.port };
2734
+ const persistedConfig: OcxConfig = { ...projectedConfig, port: persistedBinding.port };
2722
2735
  if (persistedBinding.hostname === undefined) delete persistedConfig.hostname;
2723
2736
  else persistedConfig.hostname = persistedBinding.hostname;
2724
2737
  if (persistConfigUnlocked(persistedConfig)) bumpGenerationForCooperatingConfigWrite();
2725
2738
  persistedLiveServerBinding.set(config, persistedBinding);
2726
2739
  } else {
2727
- if (persistConfigUnlocked(config)) bumpGenerationForCooperatingConfigWrite();
2740
+ if (persistConfigUnlocked(projectedConfig)) bumpGenerationForCooperatingConfigWrite();
2728
2741
  }
2742
+ adoptCustomModelCatalogMigration(config, projectedConfig);
2729
2743
  if (claudeCodeBaseline.has(config)) {
2730
2744
  claudeCodeBaseline.set(config, structuredClone(config.claudeCode));
2731
2745
  }
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable */
2
- // Generated by scripts/generate-jawcode-metadata.ts. Do not edit by hand.
2
+ // Generated by scripts/generate-model-metadata.ts. Do not edit by hand.
3
3
 
4
- export interface JawcodeModelMetadata {
4
+ export interface ModelMetadata {
5
5
  provider: string;
6
6
  id: string;
7
7
  contextWindow?: number;
@@ -27,8 +27,10 @@ const PROVIDER_ALIASES: Record<string, string> = {
27
27
  "google-antigravity": "google",
28
28
  "antigravity": "google",
29
29
  "gemini-antigravity": "google",
30
+ "deepseek": "deepseek",
30
31
  "moonshot": "moonshot",
31
32
  "zhipu-bigmodel": "zai",
33
+ "zhipu-bigmodel-coding": "zai",
32
34
  "minimax": "minimax",
33
35
  "minimax-cn": "minimax"
34
36
  } as const;
@@ -39,7 +41,7 @@ const DATA: Record<string, readonly Row[]> = {
39
41
  "anthropic": [["claude-3-5-sonnet-20240620",200000,8192,"text,image",0,null,3,15,0.3,3.75],["claude-3-5-sonnet-20241022",200000,8192,"text,image",0,null,3,15,0.3,3.75],["claude-3-haiku-20240307",200000,4096,"text,image",0,null,0.25,1.25,0.03,0.3],["claude-fable-5",1000000,128000,"text,image",1,null,10,50,1,12.5],["claude-haiku-4-5",200000,64000,"text,image",1,null,1,5,0.1,1.25],["claude-haiku-4-5-20251001",200000,64000,"text,image",1,null,1,5,0.1,1.25],["claude-opus-4-0",200000,32000,"text,image",1,null,15,75,1.5,18.75],["claude-opus-4-1",200000,32000,"text,image",1,null,15,75,1.5,18.75],["claude-opus-4-1-20250805",200000,32000,"text,image",1,null,15,75,1.5,18.75],["claude-opus-4-20250514",200000,32000,"text,image",1,null,15,75,1.5,18.75],["claude-opus-4-5",200000,64000,"text,image",1,null,5,25,0.5,6.25],["claude-opus-4-5-20251101",200000,64000,"text,image",1,null,5,25,0.5,6.25],["claude-opus-4-6",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["claude-opus-4-6[1m]",1000000,128000,"text,image",1,"claude-opus-4-6",5,25,0.5,6.25],["claude-opus-4-7",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["claude-opus-4-7[1m]",1000000,128000,"text,image",1,"claude-opus-4-7",5,25,0.5,6.25],["claude-opus-4-8",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["claude-opus-4-8[1m]",1000000,128000,"text,image",1,"claude-opus-4-8",5,25,0.5,6.25],["claude-opus-5",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["claude-sonnet-4-0",200000,64000,"text,image",1,null,3,15,0.3,3.75],["claude-sonnet-4-20250514",200000,64000,"text,image",1,null,3,15,0.3,3.75],["claude-sonnet-4-5",1000000,64000,"text,image",1,null,3,15,0.3,3.75],["claude-sonnet-4-5-20250929",1000000,64000,"text,image",1,null,3,15,0.3,3.75],["claude-sonnet-4-6",1000000,128000,"text,image",1,null,3,15,0.3,3.75],["claude-sonnet-4-6[1m]",1000000,64000,"text,image",1,"claude-sonnet-4-6",3,15,0.3,3.75],["claude-sonnet-5",1000000,128000,"text,image",1,null,2,10,0.2,2.5]],
40
42
  "azure-openai": [["gpt-4.1",1047576,32768,"text,image",0,null,2,8,0.5,0],["gpt-4o",128000,16384,"text,image",0,null,2.5,10,1.25,0],["gpt-4o-mini",128000,16384,"text,image",0,null,0.15,0.6,0.075,0],["o3",200000,100000,"text,image",1,null,2,8,0.5,0],["o3-mini",200000,100000,"text",1,null,1.1,4.4,0.55,0]],
41
43
  "cerebras": [["gemma-4-31b",131072,40960,"text,image",1,null,0.99,1.49,0,0],["gpt-oss-120b",131072,40960,"text",1,null,0.35,0.75,0,0],["llama3.1-8b",32000,8000,"text",0,null,0.1,0.1,0,0],["qwen-3-235b-a22b-instruct-2507",131000,32000,"text",0,null,0.6,1.2,0,0],["qwen-3-coder-480b",131072,32768,"text",0,null,0,0,0,0],["zai-glm-4.6",131072,32768,"text",0,null,0,0,0,0],["zai-glm-4.7",131072,40960,"text",1,null,2.25,2.75,2.25,0]],
42
- "deepseek": [["deepseek-v4-flash",1000000,384000,"text",1,null,0.14,0.28,0.0028,0],["deepseek-v4-pro",1000000,384000,"text",1,null,0.435,0.87,0.003625,0]],
44
+ "deepseek": [["deepseek-v4-flash",1048576,384000,"text",1,null,0.14,0.28,0.0028,0],["deepseek-v4-pro",1048576,384000,"text",1,null,0.435,0.87,0.003625,0]],
43
45
  "google": [["deep-research-max-preview-04-2026",131072,65536,"text,image",1,null,2,12,0.2,0],["deep-research-preview-04-2026",131072,65536,"text,image",1,null,2,12,0.2,0],["gemini-1.5-flash",1000000,8192,"text,image",0,null,0.075,0.3,0.01875,0],["gemini-1.5-flash-8b",1000000,8192,"text,image",0,null,0.0375,0.15,0.01,0],["gemini-1.5-pro",1000000,8192,"text,image",0,null,1.25,5,0.3125,0],["gemini-2.0-flash",1048576,8192,"text,image",0,null,0.1,0.4,0.025,0],["gemini-2.0-flash-lite",1048576,8192,"text,image",0,null,0.075,0.3,0,0],["gemini-2.5-computer-use-preview-10-2025",131072,65536,"text,image",1,null,1.25,10,0,0],["gemini-2.5-flash",1048576,65536,"text,image",1,null,0.3,2.5,0.03,0],["gemini-2.5-flash-lite",1048576,65536,"text,image",1,null,0.1,0.4,0.01,0],["gemini-2.5-flash-lite-preview-06-17",1048576,65536,"text,image",1,null,0.1,0.4,0.025,0],["gemini-2.5-flash-lite-preview-09-2025",1048576,65536,"text,image",1,null,0.1,0.4,0.025,0],["gemini-2.5-flash-preview-04-17",1048576,65536,"text,image",1,null,0.15,0.6,0.0375,0],["gemini-2.5-flash-preview-05-20",1048576,65536,"text,image",1,null,0.15,0.6,0.0375,0],["gemini-2.5-flash-preview-09-2025",1048576,65536,"text,image",1,null,0.3,2.5,0.075,0],["gemini-2.5-pro",1048576,65536,"text,image",1,null,1.25,10,0.125,0],["gemini-2.5-pro-preview-05-06",1048576,65536,"text,image",1,null,1.25,10,0.31,0],["gemini-2.5-pro-preview-06-05",1048576,65536,"text,image",1,null,1.25,10,0.31,0],["gemini-3-flash-preview",1048576,65536,"text,image",1,null,0.5,3,0.05,0],["gemini-3-pro-preview",1048576,65536,"text,image",1,null,2,12,0.2,0],["gemini-3.1-flash-lite",1048576,65536,"text,image",1,null,0.25,1.5,0.025,0],["gemini-3.1-flash-lite-image",65536,65536,"text,image",1,null,0.25,30,0,0],["gemini-3.1-flash-lite-preview",1048576,65536,"text,image",1,null,0.25,1.5,0.025,0],["gemini-3.1-flash-live-preview",131072,65536,"text,image",1,null,0.75,4.5,0,0],["gemini-3.1-pro-preview",1048576,65536,"text,image",1,null,2,12,0.2,0],["gemini-3.1-pro-preview-customtools",1048576,65536,"text,image",1,null,2,12,0.2,0],["gemini-3.5-flash",1048576,65536,"text,image",1,null,1.5,9,0.15,0],["gemini-3.5-flash-lite",1048576,65536,"text,image",1,null,0.3,2.5,0.03,0],["gemini-3.6-flash",1048576,65536,"text,image",1,null,1.5,7.5,0.15,0],["gemini-flash-latest",1048576,65536,"text,image",1,null,1.5,9,0.15,0],["gemini-flash-lite-latest",1048576,65536,"text,image",1,null,0.25,1.5,0.025,0],["gemini-live-2.5-flash",128000,8000,"text,image",1,null,0.5,2,0,0],["gemini-live-2.5-flash-preview-native-audio",131072,65536,"text",1,null,0.5,2,0,0],["gemini-robotics-er-1.6-preview",131072,65536,"text,image",1,null,1,5,0,0],["gemma-3-27b-it",131072,8192,"text,image",0,null,0,0,0,0],["gemma-4-26b",256000,8192,"text,image",1,null,0,0,0,0],["gemma-4-26b-a4b-it",262144,32768,"text,image",1,null,0,0,0,0],["gemma-4-26b-it",256000,8192,"text,image",1,null,0,0,0,0],["gemma-4-31b",256000,8192,"text,image",1,null,0,0,0,0],["gemma-4-31b-it",262144,32768,"text,image",1,null,0,0,0,0],["gemma-4-E2B-it",131072,8192,"text,image",1,null,0,0,0,0],["gemma-4-E4B-it",131072,8192,"text,image",1,null,0,0,0,0]],
44
46
  "minimax": [["MiniMax-M2",196608,128000,"text",1,null,0.3,1.2,0,0],["MiniMax-M2.1",204800,131072,"text",1,null,0.3,1.2,0,0],["MiniMax-M2.5",204800,131072,"text",1,null,0.3,1.2,0.03,0.375],["MiniMax-M2.5-highspeed",204800,131072,"text",1,null,0.6,2.4,0.06,0.375],["MiniMax-M2.5-lightning",204800,32000,"text",1,null,0.3,2.4,0,0],["MiniMax-M2.7",204800,131072,"text",1,null,0.3,1.2,0.06,0.375],["MiniMax-M2.7-highspeed",204800,131072,"text",1,null,0.6,2.4,0.06,0.375],["minimax-m3",512000,128000,"text,image",1,null,0.6,2.4,0.12,0],["MiniMax-M3",1000000,128000,"text,image,video",1,null,0.3,1.2,0.06,0]],
45
47
  "mistral": [["codestral-latest",256000,4096,"text",0,null,0.3,0.9,0,0],["devstral-2512",262144,262144,"text",0,null,0.4,2,0,0],["devstral-latest",262144,262144,"text",0,null,0.4,2,0,0],["devstral-medium-2507",128000,128000,"text",0,null,0.4,2,0,0],["devstral-medium-latest",262144,262144,"text",0,null,0.4,2,0,0],["devstral-small-2505",128000,128000,"text",0,null,0.1,0.3,0,0],["devstral-small-2507",128000,128000,"text",0,null,0.1,0.3,0,0],["labs-devstral-small-2512",256000,256000,"text,image",0,null,0,0,0,0],["magistral-medium-latest",128000,16384,"text",1,null,2,5,0,0],["magistral-small",128000,128000,"text",1,null,0.5,1.5,0,0],["ministral-3b-latest",128000,128000,"text",0,null,0.04,0.04,0,0],["ministral-8b-latest",128000,128000,"text",0,null,0.1,0.1,0,0],["mistral-large-2411",131072,16384,"text",0,null,2,6,0,0],["mistral-large-2512",262144,262144,"text,image",0,null,0.5,1.5,0,0],["mistral-large-latest",262144,262144,"text,image",0,null,0.5,1.5,0,0],["mistral-medium-2505",131072,131072,"text,image",0,null,0.4,2,0,0],["mistral-medium-2508",262144,262144,"text,image",0,null,0.4,2,0,0],["mistral-medium-2604",262144,262144,"text,image",1,null,1.5,7.5,0,0],["mistral-medium-latest",262144,262144,"text,image",1,null,1.5,7.5,0,0],["mistral-nemo",128000,128000,"text",0,null,0.15,0.15,0,0],["mistral-small-2506",128000,16384,"text,image",0,null,0.1,0.3,0,0],["mistral-small-2603",256000,256000,"text,image",1,null,0.15,0.6,0,0],["mistral-small-latest",256000,256000,"text,image",1,null,0.15,0.6,0,0],["open-mistral-7b",8000,8000,"text",0,null,0.25,0.25,0,0],["open-mistral-nemo",128000,128000,"text",0,null,0.15,0.15,0,0],["open-mixtral-8x22b",64000,64000,"text",0,null,2,6,0,0],["open-mixtral-8x7b",32000,32000,"text",0,null,0.7,0.7,0,0],["pixtral-12b",128000,128000,"text,image",0,null,0.15,0.15,0,0],["pixtral-large-latest",128000,128000,"text,image",0,null,2,6,0,0]],
@@ -52,24 +54,24 @@ const DATA: Record<string, readonly Row[]> = {
52
54
  "zai": [["glm-4.5",131072,98304,"text",1,null,0,0,0,0],["glm-4.5-air",131072,98304,"text",1,null,0,0,0,0],["glm-4.5-flash",131072,98304,"text",1,null,0,0,0,0],["glm-4.5v",64000,16384,"text,image",1,null,0,0,0,0],["glm-4.6",204800,131072,"text",1,null,0,0,0,0],["glm-4.6v",128000,32768,"text,image",1,null,0,0,0,0],["glm-4.7",204800,131072,"text",1,null,0,0,0,0],["glm-4.7-flash",200000,131072,"text",1,null,0,0,0,0],["glm-4.7-flashx",200000,131072,"text",1,null,0.07,0.4,0.01,0],["glm-5",204800,131072,"text",1,null,0,0,0,0],["glm-5-turbo",200000,131072,"text",1,null,0,0,0,0],["glm-5.1",200000,131072,"text",1,null,0,0,0,0],["glm-5.2",1000000,131072,"text",1,null,0,0,0,0],["glm-5v-turbo",200000,131072,"text,image",1,null,0,0,0,0]],
53
55
  };
54
56
 
55
- export function resolveJawcodeProvider(provider: string): string | undefined {
56
- return PROVIDER_ALIASES[provider];
57
+ export function resolveMetadataProvider(provider: string): string | undefined {
58
+ return PROVIDER_ALIASES[provider] ?? PROVIDER_ALIASES[provider.toLowerCase()];
57
59
  }
58
60
 
59
- export function getJawcodeModelMetadata(provider: string, modelId: string): JawcodeModelMetadata | undefined {
61
+ export function getModelMetadata(provider: string, modelId: string): ModelMetadata | undefined {
60
62
  const row = DATA[provider]?.find(r => r[0] === modelId);
61
63
  if (!row) return undefined;
62
64
  return rowToMetadata(provider, row);
63
65
  }
64
66
 
65
- export function getJawcodeModelMetadataCaseInsensitive(provider: string, modelId: string): JawcodeModelMetadata | undefined {
67
+ export function getModelMetadataCaseInsensitive(provider: string, modelId: string): ModelMetadata | undefined {
66
68
  const lower = modelId.toLowerCase();
67
69
  const row = DATA[provider]?.find(r => r[0].toLowerCase() === lower);
68
70
  if (!row) return undefined;
69
71
  return rowToMetadata(provider, row);
70
72
  }
71
73
 
72
- export function listJawcodeModelMetadata(provider: string): JawcodeModelMetadata[] {
74
+ export function listModelMetadata(provider: string): ModelMetadata[] {
73
75
  return (DATA[provider] ?? []).map(row => rowToMetadata(provider, row));
74
76
  }
75
77
 
@@ -77,7 +79,7 @@ export function listJawcodeModelMetadata(provider: string): JawcodeModelMetadata
77
79
  // official vendor price regardless of the serving provider. Exact modelId match in
78
80
  // priority-ordered vendor bundles; first nonzero cost wins.
79
81
  const COST_VENDOR_PRIORITY = ["anthropic","openai","google","moonshot","minimax","deepseek","xai","zai","mistral","cerebras","azure-openai","amazon-bedrock","xiaomi"];
80
- export function findJawcodeCostByModelId(modelId: string): { provider: string; cost: { input: number; output: number; cacheRead: number; cacheWrite: number } } | undefined {
82
+ export function findVendorCostByModelId(modelId: string): { provider: string; cost: { input: number; output: number; cacheRead: number; cacheWrite: number } } | undefined {
81
83
  for (const provider of COST_VENDOR_PRIORITY) {
82
84
  const row = DATA[provider]?.find(r => r[0] === modelId);
83
85
  if (!row) continue;
@@ -89,7 +91,7 @@ export function findJawcodeCostByModelId(modelId: string): { provider: string; c
89
91
  return undefined;
90
92
  }
91
93
 
92
- function rowToMetadata(provider: string, row: Row): JawcodeModelMetadata {
94
+ function rowToMetadata(provider: string, row: Row): ModelMetadata {
93
95
  const [id, contextWindow, maxTokens, input, reasoning, wireModelId, costInput, costOutput, costCacheRead, costCacheWrite] = row;
94
96
  const hasCost = costInput != null && costOutput != null && costCacheRead != null && costCacheWrite != null;
95
97
  return {
@@ -18,6 +18,8 @@ import {
18
18
  hermesHomeDir,
19
19
  kimiConfigPath,
20
20
  kimiHomeDir,
21
+ ompAgentDir,
22
+ ompModelsConfigPath,
21
23
  opencodeGlobalConfigPath,
22
24
  openclawConfigPath,
23
25
  openclawHomeDir,
@@ -68,6 +70,11 @@ export const INTEGRATION_CLIENTS: Record<IntegrationClientId, IntegrationClientS
68
70
  configPath: (_env = process.env, home = homedir()) => join(home, ".pi", "agent", "models.json"),
69
71
  detectDir: (_env = process.env, home = homedir()) => join(home, ".pi"),
70
72
  },
73
+ omp: {
74
+ id: "omp",
75
+ configPath: (env = process.env, home = homedir()) => ompModelsConfigPath(env, home),
76
+ detectDir: (env = process.env, home = homedir()) => ompAgentDir(env, home),
77
+ },
71
78
  hermes: {
72
79
  id: "hermes",
73
80
  configPath: (env = process.env, home = homedir()) => hermesConfigPath(env, home),
@@ -233,7 +233,7 @@ export function applyIntegration(input: IntegrationWriteInput): WriteOutcome {
233
233
  }
234
234
  if (isLoopbackOnly(clientId) && !isLoopbackHostname(input.config.hostname)) {
235
235
  return refuse(clientId, "non_loopback", classified.state,
236
- `${clientId} has nowhere to put the admission header a non-loopback bind requires, so a generated config would be rejected — and writing one by hand would not help either. Give it loopback access instead, through a tunnel or a local forwarder.`);
236
+ `The generated ${clientId} integration is loopback-only and does not emit the admission header a non-loopback bind requires. Give it loopback access instead, through a tunnel or a local forwarder.`);
237
237
  }
238
238
  if (classified.state === "conflict") {
239
239
  return refuse(clientId, "conflict", "conflict",
@@ -3,7 +3,7 @@ import https from "node:https";
3
3
 
4
4
  export type PinnedAddress = { address: string; family: number };
5
5
 
6
- export interface PinnedHttpGetOptions {
6
+ export interface PinnedHttpRequestOptions {
7
7
  headers?: HeadersInit;
8
8
  maxBytes?: number;
9
9
  idleTimeoutMs?: number;
@@ -11,15 +11,16 @@ export interface PinnedHttpGetOptions {
11
11
  context?: string;
12
12
  }
13
13
 
14
- /**
15
- * GET a URL through one previously validated address. The original hostname
16
- * remains authoritative for Host, SNI, and certificate verification.
17
- */
18
- export function pinnedHttpGet(
14
+ /** @deprecated Use {@link PinnedHttpRequestOptions}. */
15
+ export type PinnedHttpGetOptions = PinnedHttpRequestOptions;
16
+
17
+ function pinnedHttpRequest(
19
18
  url: string,
20
19
  pinned: PinnedAddress,
20
+ method: "GET" | "POST",
21
+ body: string | undefined,
21
22
  signal?: AbortSignal,
22
- options?: PinnedHttpGetOptions,
23
+ options?: PinnedHttpRequestOptions,
23
24
  ): Promise<Response> {
24
25
  const parsed = new URL(url);
25
26
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
@@ -30,6 +31,9 @@ export function pinnedHttpGet(
30
31
  const maxBytes = options?.maxBytes;
31
32
  const headers = new Headers(options?.headers);
32
33
  headers.set("host", parsed.host);
34
+ if (body !== undefined && !headers.has("content-length")) {
35
+ headers.set("content-length", String(Buffer.byteLength(body)));
36
+ }
33
37
  const requestHeaders: Record<string, string> = {};
34
38
  headers.forEach((value, key) => { requestHeaders[key] = value; });
35
39
 
@@ -51,7 +55,7 @@ export function pinnedHttpGet(
51
55
  hostname: parsed.hostname,
52
56
  port: parsed.port || (parsed.protocol === "https:" ? 443 : 80),
53
57
  path: `${parsed.pathname}${parsed.search}`,
54
- method: "GET",
58
+ method,
55
59
  headers: requestHeaders,
56
60
  ...(parsed.protocol === "https:"
57
61
  ? {
@@ -146,6 +150,33 @@ export function pinnedHttpGet(
146
150
  fail(error);
147
151
  });
148
152
  req.on("close", () => signal?.removeEventListener("abort", onAbort));
149
- req.end();
153
+ req.end(body);
150
154
  });
151
155
  }
156
+
157
+ /**
158
+ * GET a URL through one previously validated address. The original hostname
159
+ * remains authoritative for Host, SNI, and certificate verification.
160
+ */
161
+ export function pinnedHttpGet(
162
+ url: string,
163
+ pinned: PinnedAddress,
164
+ signal?: AbortSignal,
165
+ options?: PinnedHttpRequestOptions,
166
+ ): Promise<Response> {
167
+ return pinnedHttpRequest(url, pinned, "GET", undefined, signal, options);
168
+ }
169
+
170
+ /**
171
+ * POST a string body through one previously validated address. The original
172
+ * hostname remains authoritative for Host, SNI, and certificate verification.
173
+ */
174
+ export function pinnedHttpPost(
175
+ url: string,
176
+ pinned: PinnedAddress,
177
+ body: string,
178
+ signal?: AbortSignal,
179
+ options?: PinnedHttpRequestOptions,
180
+ ): Promise<Response> {
181
+ return pinnedHttpRequest(url, pinned, "POST", body, signal, options);
182
+ }
@@ -52,6 +52,9 @@ export function gracefulStopHost(hostname: string | undefined): string {
52
52
  */
53
53
  export type GracefulStopResult = boolean | "refused";
54
54
 
55
+ /** A proxy declined shutdown because a service under another home owns it (HTTP 409). */
56
+ export class ProxyOwnershipRefusedError extends Error {}
57
+
55
58
  /**
56
59
  * Ask a running proxy to stop itself via the management API (`POST /api/stop`), which
57
60
  * drains in-flight turns, restores native Codex, and cleans its pid/runtime files.
@@ -110,7 +113,7 @@ export async function stopProxy(pid: number): Promise<void> {
110
113
  if (graceful === "refused") {
111
114
  // The proxy refused on purpose (foreign service owns it). Forcing would strip shared
112
115
  // config while that service keeps the proxy alive.
113
- throw new Error(
116
+ throw new ProxyOwnershipRefusedError(
114
117
  "The running proxy refused to stop: a service installed under a different "
115
118
  + "CODEX_HOME/OPENCODEX_HOME owns it. Run the stop from that home.",
116
119
  );
@@ -6,17 +6,19 @@ import {
6
6
  providerDestinationConfigError,
7
7
  resolvePublicAddresses,
8
8
  } from "./destination-policy";
9
- import { pinnedHttpGet } from "./pinned-http";
9
+ import { pinnedHttpGet, pinnedHttpPost } from "./pinned-http";
10
10
  import { outboundProxyConfigured } from "./proxy-env";
11
11
  import { publicProviderBaseUrl } from "./provider-url";
12
12
 
13
13
  type ProviderGetInit = Omit<RequestInit, "body" | "method" | "redirect">;
14
+ type ProviderPostInit = ProviderGetInit & { body: string };
14
15
  type ProviderOutboundConfig = Pick<OcxProviderConfig, "baseUrl" | "allowPrivateNetwork"> & {
15
16
  fetch?: typeof globalThis.fetch;
16
17
  };
17
18
  export interface ProviderOutboundDependencies {
18
19
  resolveAddresses?: typeof resolvePublicAddresses;
19
20
  pinnedGet?: typeof pinnedHttpGet;
21
+ pinnedPost?: typeof pinnedHttpPost;
20
22
  }
21
23
 
22
24
  export class ProviderOutboundPolicyError extends Error {
@@ -102,13 +104,18 @@ export async function providerRedirectError(response: Response, requestUrl: stri
102
104
  return `provider returned ${response.status} redirect to ${target}; configure the final provider URL directly`;
103
105
  }
104
106
 
105
- export async function providerOutboundGet(
107
+ async function providerOutboundRequest(
106
108
  name: string,
107
109
  provider: ProviderOutboundConfig,
108
110
  url: string,
109
- init: ProviderGetInit = {},
111
+ method: "GET" | "POST",
112
+ init: ProviderGetInit | ProviderPostInit,
110
113
  dependencies: ProviderOutboundDependencies = {},
111
114
  ): Promise<Response> {
115
+ const postUrl = method === "POST" ? new URL(url) : undefined;
116
+ if (postUrl?.protocol !== undefined && postUrl.protocol !== "https:") {
117
+ throw new ProviderOutboundPolicyError("provider POST URL must use HTTPS");
118
+ }
112
119
  if (provider.fetch) {
113
120
  // A caller-owned executor cannot be peer-pinned here. This branch keeps literal/config
114
121
  // checks and redirect blocking, but does not provide the resolved-address guarantees of
@@ -128,12 +135,13 @@ export async function providerOutboundGet(
128
135
  });
129
136
  if (destinationError) throw new ProviderOutboundPolicyError(destinationError);
130
137
  }
131
- return provider.fetch(url, { ...init, method: "GET", redirect: "manual" });
138
+ return provider.fetch(url, { ...init, method, redirect: "manual" });
132
139
  }
133
- const parsed = new URL(url);
140
+ const parsed = postUrl ?? new URL(url);
134
141
  const proxyConfigured = configuredProxyFor();
135
142
  const resolveAddresses = dependencies.resolveAddresses ?? resolvePublicAddresses;
136
143
  const pinnedGet = dependencies.pinnedGet ?? pinnedHttpGet;
144
+ const pinnedPost = dependencies.pinnedPost ?? pinnedHttpPost;
137
145
  const allowPrivate = providerAllowsPrivateNetwork(name, provider);
138
146
  let resolved: Awaited<ReturnType<typeof resolvePublicAddresses>>;
139
147
  try {
@@ -150,11 +158,11 @@ export async function providerOutboundGet(
150
158
  if (!proxyConfigured) throw error;
151
159
  warnProxyBoundaryOnce();
152
160
  warnProxyDnsDegradationOnce();
153
- return globalThis.fetch(url, { ...init, method: "GET", redirect: "manual" });
161
+ return globalThis.fetch(url, { ...init, method, redirect: "manual" });
154
162
  }
155
163
  if (proxyConfigured && !resolved.privateNetwork) {
156
164
  warnProxyBoundaryOnce();
157
- return globalThis.fetch(url, { ...init, method: "GET", redirect: "manual" });
165
+ return globalThis.fetch(url, { ...init, method, redirect: "manual" });
158
166
  }
159
167
  if (proxyConfigured && resolved.privateNetwork && !noProxyMatches(parsed)) {
160
168
  const hostname = normalizeProxyHostname(parsed.hostname);
@@ -162,9 +170,34 @@ export async function providerOutboundGet(
162
170
  `provider URL resolves to a private-network destination; add ${hostname} to NO_PROXY before using allowPrivateNetwork with an outbound proxy`,
163
171
  );
164
172
  }
165
- return pinnedGet(url, pickPinnedAddress(resolved.addresses), init.signal ?? undefined, {
173
+ const requestOptions = {
166
174
  headers: init.headers,
167
175
  rejectUnauthorized: true,
168
176
  context: "provider response",
169
- });
177
+ };
178
+ const pinned = pickPinnedAddress(resolved.addresses);
179
+ if (method === "POST") {
180
+ return pinnedPost(url, pinned, (init as ProviderPostInit).body, init.signal ?? undefined, requestOptions);
181
+ }
182
+ return pinnedGet(url, pinned, init.signal ?? undefined, requestOptions);
183
+ }
184
+
185
+ export async function providerOutboundGet(
186
+ name: string,
187
+ provider: ProviderOutboundConfig,
188
+ url: string,
189
+ init: ProviderGetInit = {},
190
+ dependencies: ProviderOutboundDependencies = {},
191
+ ): Promise<Response> {
192
+ return providerOutboundRequest(name, provider, url, "GET", init, dependencies);
193
+ }
194
+
195
+ export async function providerOutboundPost(
196
+ name: string,
197
+ provider: ProviderOutboundConfig,
198
+ url: string,
199
+ init: ProviderPostInit,
200
+ dependencies: ProviderOutboundDependencies = {},
201
+ ): Promise<Response> {
202
+ return providerOutboundRequest(name, provider, url, "POST", init, dependencies);
170
203
  }
@@ -52,8 +52,8 @@ function decodeCursorJwtPayload(token: string): CursorJwtPayload | undefined {
52
52
  /** Build OAuthCredentials from Cursor tokens, extracting stable identity from JWT `sub` for multiauth. */
53
53
  export function credentialsFromCursorTokens(accessToken: string, refreshToken: string): OAuthCredentials {
54
54
  const payload = decodeCursorJwtPayload(accessToken) ?? decodeCursorJwtPayload(refreshToken);
55
- const accountId = typeof payload?.sub === "string" && payload.sub.length > 0 ? payload.sub : undefined;
56
- const email = typeof payload?.email === "string" && payload.email.length > 0 ? payload.email.toLowerCase() : undefined;
55
+ const accountId = cursorJwtIdentity(payload?.sub);
56
+ const email = cursorJwtEmail(payload?.email);
57
57
  return {
58
58
  access: accessToken,
59
59
  refresh: refreshToken,
@@ -63,10 +63,24 @@ export function credentialsFromCursorTokens(accessToken: string, refreshToken: s
63
63
  };
64
64
  }
65
65
 
66
+ /** Coerce JWT `sub` (string or safe integer) into a stable multiauth account id. */
67
+ function cursorJwtIdentity(value: unknown): string | undefined {
68
+ if (typeof value === "string" && value.length > 0) return value;
69
+ if (typeof value === "number" && Number.isSafeInteger(value)) return String(value);
70
+ return undefined;
71
+ }
72
+
73
+ function cursorJwtEmail(value: unknown): string | undefined {
74
+ if (typeof value !== "string" || value.length === 0) return undefined;
75
+ return value.toLowerCase();
76
+ }
77
+
66
78
  /** Generate PKCE params + the cursor.com deep-link login URL (challenge only — never the verifier). */
67
- export async function generateCursorAuthParams(): Promise<CursorAuthParams> {
79
+ export async function generateCursorAuthParams(_opts?: { forceLogin?: boolean }): Promise<CursorAuthParams> {
68
80
  const { verifier, challenge } = await generatePKCE();
69
81
  const uuid = crypto.randomUUID();
82
+ // Cursor's deep-control page has no documented account-picker query; only the stable
83
+ // PKCE params are sent. forceLogin is honored only in loginCursor instructions.
70
84
  const params = new URLSearchParams({ challenge, uuid, mode: "login", redirectTarget: "cli" });
71
85
  return { verifier, challenge, uuid, loginUrl: `${CURSOR_LOGIN_URL}?${params.toString()}` };
72
86
  }
@@ -139,9 +153,15 @@ export async function pollCursorAuth(
139
153
  export async function loginCursor(
140
154
  ctrl: OAuthController,
141
155
  pollBaseDelayMs: number = POLL_BASE_DELAY_MS,
156
+ opts?: { forceLogin?: boolean },
142
157
  ): Promise<OAuthCredentials> {
143
- const { verifier, uuid, loginUrl } = await generateCursorAuthParams();
144
- ctrl.onAuth?.({ url: loginUrl, instructions: "Approve the Cursor login in your browser, then return here." });
158
+ const { verifier, uuid, loginUrl } = await generateCursorAuthParams({ forceLogin: opts?.forceLogin === true });
159
+ ctrl.onAuth?.({
160
+ url: loginUrl,
161
+ instructions: opts?.forceLogin
162
+ ? "Log out of Cursor in the browser (or use a private window), sign in as the account you want to add, then approve and return here."
163
+ : "Approve the Cursor login in your browser, then return here.",
164
+ });
145
165
  ctrl.onProgress?.("Waiting for Cursor login approval…");
146
166
  const { accessToken, refreshToken } = await pollCursorAuth(uuid, verifier, ctrl.signal, pollBaseDelayMs);
147
167
  return credentialsFromCursorTokens(accessToken, refreshToken);