@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.
- package/gui/dist/assets/{index-Bk-PN-70.css → index-BA1dgo4q.css} +1 -1
- package/gui/dist/assets/index-DDZpgzKk.js +70 -0
- package/gui/dist/index.html +2 -2
- package/package.json +6 -4
- package/src/adapters/anthropic.ts +10 -2
- package/src/adapters/cursor/native-exec-common.ts +6 -2
- package/src/adapters/google-antigravity-replay.ts +3 -2
- package/src/adapters/google-antigravity-wire.ts +38 -6
- package/src/adapters/google.ts +57 -10
- package/src/adapters/openai-chat.ts +19 -2
- package/src/claude/model-info.ts +1 -1
- package/src/cli/account-auth.ts +3 -1
- package/src/cli/account-catalog-refresh.ts +14 -0
- package/src/cli/account-extended.ts +15 -1
- package/src/cli/codex-shim-readiness.ts +69 -0
- package/src/cli/combo.ts +5 -0
- package/src/cli/config-command.ts +33 -4
- package/src/cli/doctor.ts +21 -0
- package/src/cli/export-command.ts +6 -6
- package/src/cli/help.ts +6 -6
- package/src/cli/index.ts +22 -5
- package/src/cli/models-runtime.ts +13 -1
- package/src/cli/provider.ts +7 -0
- package/src/clients/config-export.ts +67 -5
- package/src/codex/account-lifecycle.ts +99 -10
- package/src/codex/auth-api.ts +222 -34
- package/src/codex/catalog/account-models.ts +9 -4
- package/src/codex/catalog/aggregation.ts +41 -8
- package/src/codex/catalog/bundled.ts +54 -22
- package/src/codex/catalog/effort.ts +47 -20
- package/src/codex/catalog/kinds.ts +2 -0
- package/src/codex/catalog/metadata.ts +70 -18
- package/src/codex/catalog/native-models.ts +7 -0
- package/src/codex/catalog/parsing.ts +29 -11
- package/src/codex/catalog/provider-fetch.ts +335 -50
- package/src/codex/catalog/sync.ts +588 -126
- package/src/codex/catalog-refresh-status.ts +87 -0
- package/src/codex/catalog-write-serialization.ts +2 -1
- package/src/codex/catalog.ts +4 -3
- package/src/codex/convergence-types.ts +1 -1
- package/src/codex/convergence.ts +190 -52
- package/src/codex/custom-model-catalog-migration.ts +176 -0
- package/src/codex/features.ts +79 -1
- package/src/codex/history-job.ts +81 -1
- package/src/codex/history-lock.ts +2 -1
- package/src/codex/history-provider.ts +4 -3
- package/src/codex/inject.ts +56 -12
- package/src/codex/model-cache.ts +50 -10
- package/src/codex/transition-state.ts +10 -2
- package/src/codex/user-identity.ts +110 -2
- package/src/combos/index.ts +3 -0
- package/src/combos/types.ts +75 -9
- package/src/config.ts +26 -12
- package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +12 -10
- package/src/integrations/registry.ts +7 -0
- package/src/integrations/writer.ts +1 -1
- package/src/lib/pinned-http.ts +40 -9
- package/src/lib/process-control.ts +4 -1
- package/src/lib/provider-outbound.ts +42 -9
- package/src/oauth/cursor.ts +25 -5
- package/src/oauth/index.ts +59 -45
- package/src/providers/antigravity-models.ts +115 -3
- package/src/providers/context-cap.ts +13 -5
- package/src/providers/model-discovery-limits.ts +16 -0
- package/src/providers/model-discovery.ts +14 -15
- package/src/providers/provider-id-rewrite.ts +29 -0
- package/src/providers/quota.ts +93 -19
- package/src/providers/registry.ts +16 -5
- package/src/providers/slug-codec.ts +13 -6
- package/src/reasoning-effort.ts +22 -0
- package/src/router.ts +0 -20
- package/src/routing/history/indexer.ts +54 -39
- package/src/routing/quota.ts +77 -56
- package/src/server/index.ts +32 -7
- package/src/server/management/combo-routes.ts +43 -19
- package/src/server/management/config-routes.ts +115 -20
- package/src/server/management/model-routes.ts +10 -7
- package/src/server/management/model-rows.ts +6 -2
- package/src/server/management/oauth-account-routes.ts +12 -0
- package/src/server/management/provider-routes.ts +114 -40
- package/src/server/management/routing-profile-routes.ts +0 -22
- package/src/server/management-api.ts +7 -25
- package/src/server/responses/core.ts +3 -3
- package/src/server/responses/policy-fallback.ts +152 -0
- package/src/server/responses.ts +3 -2
- package/src/types.ts +15 -2
- package/src/usage/cost.ts +0 -0
- package/src/vision/describe.ts +3 -1
- package/src/vision/index.ts +17 -6
- package/src/vision/reasoning.ts +55 -0
- package/src/web-search/parse.ts +15 -3
- package/gui/dist/assets/index-BynIEIV-.js +0 -70
package/src/combos/index.ts
CHANGED
|
@@ -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,
|
package/src/combos/types.ts
CHANGED
|
@@ -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("/")
|
|
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
|
|
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)
|
|
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.
|
|
1057
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
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 = { ...
|
|
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(
|
|
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-
|
|
2
|
+
// Generated by scripts/generate-model-metadata.ts. Do not edit by hand.
|
|
3
3
|
|
|
4
|
-
export interface
|
|
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",
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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):
|
|
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
|
-
|
|
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",
|
package/src/lib/pinned-http.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
16
|
-
|
|
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?:
|
|
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
|
|
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
|
|
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
|
-
|
|
107
|
+
async function providerOutboundRequest(
|
|
106
108
|
name: string,
|
|
107
109
|
provider: ProviderOutboundConfig,
|
|
108
110
|
url: string,
|
|
109
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
}
|
package/src/oauth/cursor.ts
CHANGED
|
@@ -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 =
|
|
56
|
-
const email =
|
|
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?.({
|
|
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);
|