@bitkyc08/opencodex 2.17.0 → 2.18.2
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-DOKr6RBR.js → index-CXI1262_.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/client-fingerprint.ts +2 -2
- package/src/adapters/cursor/live-transport.ts +17 -5
- package/src/adapters/cursor/protobuf-events.ts +662 -20
- package/src/adapters/cursor/tool-definitions.ts +12 -6
- package/src/adapters/google.ts +21 -2
- package/src/bridge.ts +12 -2
- package/src/cli/index.ts +11 -0
- package/src/codex/app-server-processes.ts +3 -3
- package/src/codex/user-identity.ts +36 -6
- package/src/config.ts +0 -2
- package/src/generated/compatibility-version.json +37 -33
- package/src/lib/token-estimate.ts +19 -2
- package/src/lib/windows-elevation.ts +37 -0
- package/src/lib/windows-secret-acl.ts +7 -0
- package/src/lib/windows-text.ts +106 -0
- package/src/lib/windows-user-principal.ts +0 -2
- package/src/oauth/index.ts +1 -1
- package/src/oauth/store.ts +32 -18
- package/src/providers/antigravity-models.ts +25 -5
- package/src/providers/free-directory.ts +1 -1
- package/src/providers/registry.ts +4 -3
- package/src/server/index.ts +5 -1
- package/src/server/management/logs-usage-routes.ts +7 -22
- package/src/server/request-log.ts +48 -3
- package/src/server/responses/core.ts +38 -13
- package/src/server/responses/encrypted-payload.ts +58 -38
- package/src/server/responses/fetch-helpers.ts +12 -4
- package/src/server/responses/policy-fallback.ts +13 -2
- package/src/server/responses/ws-upstream.ts +115 -6
- package/src/service-manager-probe.ts +21 -34
- package/src/service.ts +233 -25
- package/src/tray/windows.ts +0 -2
- package/src/update/job.ts +2 -2
- package/src/usage/summary.ts +21 -4
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decode bounded text emitted by Windows system tools.
|
|
3
|
+
*
|
|
4
|
+
* [Decision Log]
|
|
5
|
+
* - Purpose: preserve non-ASCII paths when a Windows tool writes the active
|
|
6
|
+
* legacy code page instead of UTF-8.
|
|
7
|
+
* - Existing constraint: generated service assets may be UTF-16, while
|
|
8
|
+
* redirected `schtasks` output follows the Windows locale on affected hosts.
|
|
9
|
+
* - Alternatives considered: replacement-character heuristics and a new
|
|
10
|
+
* iconv dependency. The former can reinterpret valid text; the latter widens
|
|
11
|
+
* the install/security surface for two small, already-supported codecs.
|
|
12
|
+
* - Choice: recognize UTF-16 first, accept only strict UTF-8 next, then use the
|
|
13
|
+
* locale-appropriate WHATWG decoder (CP949 through `euc-kr`, or Windows-1252
|
|
14
|
+
* only for locales that actually use that family). Unknown/unsupported
|
|
15
|
+
* locales fail back to the old replacement-preserving UTF-8 result instead
|
|
16
|
+
* of guessing another code page or throwing in diagnostics.
|
|
17
|
+
* - Impact: decoding stays dependency-free and bounded, but this deliberately
|
|
18
|
+
* does not guess arbitrary OEM code pages that the runtime cannot identify.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
function trimWindowsText(value: string): string {
|
|
22
|
+
return value.replace(/^\uFEFF/, "").trim();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function currentWindowsLocale(): string {
|
|
26
|
+
try {
|
|
27
|
+
return Intl.DateTimeFormat().resolvedOptions().locale;
|
|
28
|
+
} catch {
|
|
29
|
+
return "en-US";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function decodeStrict(buffer: Uint8Array, encoding: string): string | null {
|
|
34
|
+
try {
|
|
35
|
+
return trimWindowsText(new TextDecoder(encoding, { fatal: true }).decode(buffer));
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function decodeUtf16Be(buffer: Uint8Array): string {
|
|
42
|
+
const payloadLength = buffer.length - 2;
|
|
43
|
+
const swapped = Buffer.alloc(payloadLength - (payloadLength % 2));
|
|
44
|
+
for (let i = 2; i + 1 < buffer.length; i += 2) {
|
|
45
|
+
swapped[i - 2] = buffer[i + 1]!;
|
|
46
|
+
swapped[i - 1] = buffer[i]!;
|
|
47
|
+
}
|
|
48
|
+
return trimWindowsText(swapped.toString("utf16le"));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* CP949 is exposed by the Encoding Standard under the `euc-kr` label. Keep the
|
|
53
|
+
* Western fallback deliberately narrow: treating CP932, CP1250, or CP1251
|
|
54
|
+
* bytes as Windows-1252 can fabricate a different valid-looking filesystem
|
|
55
|
+
* path, which is worse than the previous replacement-character refusal.
|
|
56
|
+
*/
|
|
57
|
+
function legacyEncodingForLocale(locale: string): "euc-kr" | "windows-1252" | null {
|
|
58
|
+
const language = locale.trim().split(/[-_]/, 1)[0]?.toLowerCase();
|
|
59
|
+
if (language === "ko") return "euc-kr";
|
|
60
|
+
if (language && WINDOWS_1252_LANGUAGES.has(language)) return "windows-1252";
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const WINDOWS_1252_LANGUAGES = new Set([
|
|
65
|
+
"af", "br", "ca", "co", "cy", "da", "de", "en", "es", "eu", "fi", "fo", "fr",
|
|
66
|
+
"ga", "gd", "gl", "id", "is", "it", "lb", "ms", "nl", "no", "oc", "pt", "sq",
|
|
67
|
+
"sv", "sw",
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
export interface WindowsTextDecodeOptions {
|
|
71
|
+
/** Test seam and explicit locale override; production uses the active Intl locale. */
|
|
72
|
+
readonly locale?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function decodeWindowsTextBytes(
|
|
76
|
+
buffer: Uint8Array,
|
|
77
|
+
options: WindowsTextDecodeOptions = {},
|
|
78
|
+
): string {
|
|
79
|
+
if (buffer.length === 0) return "";
|
|
80
|
+
|
|
81
|
+
const bomUtf16Le = buffer.length >= 2 && buffer[0] === 0xff && buffer[1] === 0xfe;
|
|
82
|
+
const bomUtf16Be = buffer.length >= 2 && buffer[0] === 0xfe && buffer[1] === 0xff;
|
|
83
|
+
const looksUtf16Le = buffer.length >= 4
|
|
84
|
+
&& buffer[1] === 0x00
|
|
85
|
+
&& buffer[3] === 0x00
|
|
86
|
+
&& buffer[0] !== 0x00;
|
|
87
|
+
|
|
88
|
+
if (bomUtf16Le || looksUtf16Le) {
|
|
89
|
+
return trimWindowsText(Buffer.from(buffer).toString("utf16le"));
|
|
90
|
+
}
|
|
91
|
+
if (bomUtf16Be) return decodeUtf16Be(buffer);
|
|
92
|
+
|
|
93
|
+
const utf8 = decodeStrict(buffer, "utf-8");
|
|
94
|
+
if (utf8 !== null) return utf8;
|
|
95
|
+
|
|
96
|
+
const locale = options.locale ?? currentWindowsLocale();
|
|
97
|
+
const legacyEncoding = legacyEncodingForLocale(locale);
|
|
98
|
+
if (legacyEncoding !== null) {
|
|
99
|
+
const legacy = decodeStrict(buffer, legacyEncoding);
|
|
100
|
+
if (legacy !== null) return legacy;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Preserve the previous fail-soft behavior when the runtime lacks a codec or
|
|
104
|
+
// the bytes are malformed even for the selected Windows code page.
|
|
105
|
+
return trimWindowsText(new TextDecoder("utf-8").decode(buffer));
|
|
106
|
+
}
|
package/src/oauth/index.ts
CHANGED
|
@@ -1124,7 +1124,7 @@ export async function runLogin(
|
|
|
1124
1124
|
await (deps.saveAccountCredential ?? saveAccountCredential)(provider, opts.reauthAccountId, cred);
|
|
1125
1125
|
} else {
|
|
1126
1126
|
await (deps.saveCredential ?? saveCredential)(provider, cred, {
|
|
1127
|
-
preserveIdentityless:
|
|
1127
|
+
preserveIdentityless: opts?.forceLogin === true,
|
|
1128
1128
|
});
|
|
1129
1129
|
}
|
|
1130
1130
|
if (provider !== "chatgpt") {
|
package/src/oauth/store.ts
CHANGED
|
@@ -10,10 +10,11 @@
|
|
|
10
10
|
* Exceptions:
|
|
11
11
|
* - `chatgpt` stays single-slot (always replaced): codex-auth-api uses it as a scratch slot
|
|
12
12
|
* for Codex pool logins, which have their own ledger (codex-accounts.json).
|
|
13
|
-
* - Credentials without identity (no accountId/email) replace the active slot
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* extracts JWT `sub`
|
|
13
|
+
* - Credentials without identity (no accountId/email) replace the active slot on a normal
|
|
14
|
+
* login: their refresh tokens rotate, so a derived id would duplicate the same human on every
|
|
15
|
+
* re-login. An explicit add-account login instead preserves the prior slot and appends a
|
|
16
|
+
* distinct one. Kimi extracts JWT `user_id`/`sub` as accountId; Cursor extracts JWT `sub` —
|
|
17
|
+
* both append distinct identified accounts under multiauth.
|
|
17
18
|
*/
|
|
18
19
|
import { createHash, randomUUID } from "node:crypto";
|
|
19
20
|
import { chmodSync, closeSync, copyFileSync, existsSync, fstatSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
@@ -300,6 +301,17 @@ function newAccountId(cred: OAuthCredentials): string {
|
|
|
300
301
|
return createHash("sha256").update(identity).digest("hex").slice(0, 32);
|
|
301
302
|
}
|
|
302
303
|
|
|
304
|
+
/** Allocate a persisted slot id without reusing any existing account's ownership key. */
|
|
305
|
+
function distinctAccountId(cred: OAuthCredentials, accounts: readonly ProviderAccount[]): string {
|
|
306
|
+
const base = newAccountId(cred);
|
|
307
|
+
const occupied = new Set(accounts.map(account => account.id));
|
|
308
|
+
if (!occupied.has(base)) return base;
|
|
309
|
+
for (let suffix = 1; ; suffix += 1) {
|
|
310
|
+
const candidate = `${base}-${suffix}`;
|
|
311
|
+
if (!occupied.has(candidate)) return candidate;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
303
315
|
function normalizeAccount(value: unknown): ProviderAccount | null {
|
|
304
316
|
if (!value || typeof value !== "object") return null;
|
|
305
317
|
const candidate = value as Partial<ProviderAccount>;
|
|
@@ -473,7 +485,8 @@ export function getCredential(provider: string): OAuthCredentials | null {
|
|
|
473
485
|
* Persist a credential as the ACTIVE account. Identity-matching (accountId ?? email) upserts
|
|
474
486
|
* the same human's slot; a new identity appends a new account. Credentials without identity
|
|
475
487
|
* (rotating refresh tokens would fabricate duplicates) and single-slot providers replace the
|
|
476
|
-
* active slot / whole set instead.
|
|
488
|
+
* active slot / whole set instead. An explicit add-account login can preserve the legacy slot;
|
|
489
|
+
* an identity-less credential then gets its deterministic refresh-derived account id.
|
|
477
490
|
*/
|
|
478
491
|
export async function saveCredential(
|
|
479
492
|
provider: string,
|
|
@@ -508,18 +521,24 @@ export async function saveCredential(
|
|
|
508
521
|
delete active.needsReauth;
|
|
509
522
|
return;
|
|
510
523
|
}
|
|
511
|
-
const id =
|
|
524
|
+
const id = distinctAccountId(safe, set.accounts);
|
|
525
|
+
set.accounts.push({ id, credential: safe, addedAt: Date.now() });
|
|
526
|
+
set.activeAccountId = id;
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
if (opts.preserveIdentityless) {
|
|
530
|
+
const id = distinctAccountId(safe, set.accounts);
|
|
512
531
|
set.accounts.push({ id, credential: safe, addedAt: Date.now() });
|
|
513
532
|
set.activeAccountId = id;
|
|
514
533
|
return;
|
|
515
534
|
}
|
|
516
|
-
// No identity: replace the active slot in place
|
|
535
|
+
// No identity during a normal login: replace the active slot in place.
|
|
517
536
|
const active = set.accounts.find(a => a.id === set.activeAccountId);
|
|
518
537
|
if (active) {
|
|
519
538
|
active.credential = safe;
|
|
520
539
|
delete active.needsReauth;
|
|
521
540
|
} else {
|
|
522
|
-
const id =
|
|
541
|
+
const id = distinctAccountId(safe, set.accounts);
|
|
523
542
|
set.accounts.push({ id, credential: safe, addedAt: Date.now() });
|
|
524
543
|
set.activeAccountId = id;
|
|
525
544
|
}
|
|
@@ -541,15 +560,7 @@ export async function upsertCredentialByIdentity(
|
|
|
541
560
|
}
|
|
542
561
|
return await mutateStore(store => {
|
|
543
562
|
const set = store[provider];
|
|
544
|
-
const
|
|
545
|
-
if (safe.accountId) {
|
|
546
|
-
if (account.credential.accountId) return account.credential.accountId === safe.accountId;
|
|
547
|
-
return Boolean(
|
|
548
|
-
safe.email
|
|
549
|
-
&& account.credential.email
|
|
550
|
-
&& account.credential.email.toLowerCase() === safe.email.toLowerCase(),
|
|
551
|
-
);
|
|
552
|
-
}
|
|
563
|
+
const matchesEmailOnly = (account: ProviderAccount): boolean => {
|
|
553
564
|
if (account.credential.accountId) return false;
|
|
554
565
|
return Boolean(
|
|
555
566
|
safe.email
|
|
@@ -557,7 +568,10 @@ export async function upsertCredentialByIdentity(
|
|
|
557
568
|
&& account.credential.email.toLowerCase() === safe.email.toLowerCase(),
|
|
558
569
|
);
|
|
559
570
|
};
|
|
560
|
-
const existing =
|
|
571
|
+
const existing = safe.accountId
|
|
572
|
+
? set?.accounts.find(account => account.credential.accountId === safe.accountId)
|
|
573
|
+
?? set?.accounts.find(matchesEmailOnly)
|
|
574
|
+
: set?.accounts.find(matchesEmailOnly);
|
|
561
575
|
if (existing && set) {
|
|
562
576
|
existing.credential = safe;
|
|
563
577
|
delete existing.needsReauth;
|
|
@@ -14,6 +14,13 @@ import { isValidModelDiscoveryModelId, MODEL_DISCOVERY_MAX_MODELS } from "./mode
|
|
|
14
14
|
/** Current Antigravity Flash generation. */
|
|
15
15
|
const GEMINI_FLASH_CURRENT = "gemini-3.7-flash";
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Wire ID that CCA actually accepts for the current Flash generation.
|
|
19
|
+
* Google renamed the model to include a `-tiered` suffix; the picker-visible
|
|
20
|
+
* ID stays `gemini-3.7-flash` (stripped by `pickerModelIdForDiscoveredWireId`).
|
|
21
|
+
*/
|
|
22
|
+
const GEMINI_FLASH_WIRE_ID = "gemini-3.7-flash-tiered";
|
|
23
|
+
|
|
17
24
|
/**
|
|
18
25
|
* Retired Flash ids → the reasoning tier they used to encode.
|
|
19
26
|
*
|
|
@@ -42,7 +49,7 @@ const RETIRED_FLASH_TIERS: Record<string, string> = {
|
|
|
42
49
|
};
|
|
43
50
|
|
|
44
51
|
const ANTIGRAVITY_WIRE_MODELS = [
|
|
45
|
-
"gemini-3.7-flash",
|
|
52
|
+
"gemini-3.7-flash-tiered",
|
|
46
53
|
"gemini-3.1-pro-low",
|
|
47
54
|
"gemini-pro-agent",
|
|
48
55
|
"gemini-3.1-flash-image",
|
|
@@ -133,6 +140,19 @@ const ANTIGRAVITY_THINKING_LEVEL_MODELS: Record<string, string> = {
|
|
|
133
140
|
// Flash generation, where it is an error rather than a quieter tier.
|
|
134
141
|
const ANTIGRAVITY_THINKING_LEVELS = new Set(["low", "medium", "high"]);
|
|
135
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Picker-visible model IDs whose CCA wire ID differs (the `-tiered` rename).
|
|
145
|
+
* Models not listed here use themselves as the wire ID.
|
|
146
|
+
*/
|
|
147
|
+
const ANTIGRAVITY_PICKER_TO_WIRE: Record<string, string> = {
|
|
148
|
+
"gemini-3.7-flash": GEMINI_FLASH_WIRE_ID,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/** Map a picker-visible base model to its CCA wire ID. Identity when no mapping exists. */
|
|
152
|
+
function pickerToWireId(pickerId: string): string {
|
|
153
|
+
return ANTIGRAVITY_PICKER_TO_WIRE[pickerId] ?? pickerId;
|
|
154
|
+
}
|
|
155
|
+
|
|
136
156
|
function resolveAntigravityThinkingLevel(effort: string): string | undefined {
|
|
137
157
|
if (effort === "xhigh" || effort === "max" || effort === "ultra") return "high";
|
|
138
158
|
return ANTIGRAVITY_THINKING_LEVELS.has(effort) ? effort : undefined;
|
|
@@ -157,7 +177,7 @@ const ANTIGRAVITY_COMPATIBILITY_MODEL_ALIASES: Record<string, string> = {
|
|
|
157
177
|
// because `parseAntigravityAvailableModels` uses THIS map to keep a stale CCA
|
|
158
178
|
// payload from republishing a dead wire id as a picker row.
|
|
159
179
|
...Object.fromEntries(
|
|
160
|
-
Object.keys(RETIRED_FLASH_TIERS).map(retired => [retired,
|
|
180
|
+
Object.keys(RETIRED_FLASH_TIERS).map(retired => [retired, GEMINI_FLASH_WIRE_ID]),
|
|
161
181
|
),
|
|
162
182
|
};
|
|
163
183
|
|
|
@@ -182,7 +202,7 @@ function isKnownAntigravityPickerModelId(value: string): boolean {
|
|
|
182
202
|
|
|
183
203
|
// Context windows from the upstream `:fetchAvailableModels` maxTokens per model.
|
|
184
204
|
const ANTIGRAVITY_WIRE_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
|
|
185
|
-
"gemini-3.7-flash": 1_048_576,
|
|
205
|
+
"gemini-3.7-flash-tiered": 1_048_576,
|
|
186
206
|
"gemini-3.1-pro-low": 1_048_576,
|
|
187
207
|
"gemini-pro-agent": 1_048_576,
|
|
188
208
|
"gemini-3.1-flash-image": 1_048_576,
|
|
@@ -357,7 +377,7 @@ export function resolveAntigravityEffortWireModel(
|
|
|
357
377
|
const retiredTier = retiredAntigravityFlashTier(modelId);
|
|
358
378
|
if (retiredTier) {
|
|
359
379
|
return {
|
|
360
|
-
wireModelId:
|
|
380
|
+
wireModelId: GEMINI_FLASH_WIRE_ID,
|
|
361
381
|
thinkingLevel: effort ? resolveAntigravityThinkingLevel(effort) ?? retiredTier : retiredTier,
|
|
362
382
|
};
|
|
363
383
|
}
|
|
@@ -372,7 +392,7 @@ export function resolveAntigravityEffortWireModel(
|
|
|
372
392
|
const defaultLevel = ANTIGRAVITY_THINKING_LEVEL_MODELS[modelId];
|
|
373
393
|
if (defaultLevel) {
|
|
374
394
|
return {
|
|
375
|
-
wireModelId: modelId,
|
|
395
|
+
wireModelId: pickerToWireId(modelId),
|
|
376
396
|
thinkingLevel: effort ? resolveAntigravityThinkingLevel(effort) ?? defaultLevel : defaultLevel,
|
|
377
397
|
};
|
|
378
398
|
}
|
|
@@ -82,7 +82,7 @@ const CONNECTABLE: Record<string, ConnectableOverride> = {
|
|
|
82
82
|
"cloudflare-ai": openAi("https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/v1", "https://dash.cloudflare.com/?to=/:account/ai/workers-ai", { supportLevel: "supported", verification: "official", documentationUrl: "https://developers.cloudflare.com/workers-ai/configuration/open-ai-compatibility/", discovery: "static", liveModels: false, models: ["@cf/meta/llama-3.3-70b-instruct-fp8-fast", "@cf/qwen/qwq-32b"] }),
|
|
83
83
|
cohere: openAi("https://api.cohere.com/compatibility/v1", "https://dashboard.cohere.com/api-keys", { supportLevel: "supported", verification: "official", documentationUrl: "https://docs.cohere.com/reference/list-models", modelsUrl: "https://api.cohere.com/compatibility/v1/models" }),
|
|
84
84
|
friendliai: openAi("https://api.friendli.ai/serverless/v1", "https://suite.friendli.ai", { modelsUrl: "https://api.friendli.ai/serverless/v1/models" }),
|
|
85
|
-
gemini: { baseUrl: "https://generativelanguage.googleapis.com", dashboardUrl: "https://aistudio.google.com/apikey", adapter: "google", authKind: "key", supportLevel: "supported", verification: "official", documentationUrl: "https://ai.google.dev/api/models", lastVerified: LAST_VERIFIED, discovery: "live", liveModels: true, googleMode: "ai-studio", models: ["gemini-3.6-flash", "gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.1-pro-preview"] },
|
|
85
|
+
gemini: { baseUrl: "https://generativelanguage.googleapis.com", dashboardUrl: "https://aistudio.google.com/apikey", adapter: "google", authKind: "key", supportLevel: "supported", verification: "official", documentationUrl: "https://ai.google.dev/api/models", lastVerified: LAST_VERIFIED, discovery: "live", liveModels: true, googleMode: "ai-studio", models: ["gemini-3.7-flash", "gemini-3.6-flash", "gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.1-pro-preview"] },
|
|
86
86
|
"github-models": openAi("https://models.github.ai/inference", "https://github.com/settings/tokens", { supportLevel: "supported", verification: "official", documentationUrl: "https://docs.github.com/en/github-models/prototyping-with-ai-models", discovery: "static", liveModels: false, models: ["openai/gpt-4.1", "meta/llama-4-scout-17b-16e-instruct"] }),
|
|
87
87
|
groq: openAi("https://api.groq.com/openai/v1", "https://console.groq.com/keys", { supportLevel: "supported", verification: "official", documentationUrl: "https://console.groq.com/docs/api-reference#models" }),
|
|
88
88
|
hackclub: openAi("https://ai.hackclub.com/proxy/v1", "https://ai.hackclub.com", { modelsUrl: "https://ai.hackclub.com/proxy/v1/models" }),
|
|
@@ -1438,12 +1438,13 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
1438
1438
|
// devlog/_plan/260710_provider_hardening/001_research_frontier.md.
|
|
1439
1439
|
{
|
|
1440
1440
|
id: "google", label: "Google Gemini", adapter: "google", baseUrl: "https://generativelanguage.googleapis.com", authKind: "key", featured: true,
|
|
1441
|
-
dashboardUrl: "https://aistudio.google.com/apikey", defaultModel: "gemini-3.5-flash", models: ["gemini-3.6-flash", "gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.1-pro-preview"],
|
|
1442
|
-
modelContextWindows: { "gemini-3.6-flash": 1_048_576, "gemini-3.5-flash": 1_000_000, "gemini-3.5-flash-lite": 1_048_576 },
|
|
1443
|
-
modelInputModalities: { "gemini-3.6-flash": ["text", "image"], "gemini-3.5-flash-lite": ["text", "image"] },
|
|
1441
|
+
dashboardUrl: "https://aistudio.google.com/apikey", defaultModel: "gemini-3.5-flash", models: ["gemini-3.6-flash", "gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.1-pro-preview", "gemini-3.7-flash"],
|
|
1442
|
+
modelContextWindows: { "gemini-3.6-flash": 1_048_576, "gemini-3.5-flash": 1_000_000, "gemini-3.5-flash-lite": 1_048_576, "gemini-3.7-flash": 1_048_576 },
|
|
1443
|
+
modelInputModalities: { "gemini-3.6-flash": ["text", "image"], "gemini-3.5-flash-lite": ["text", "image"], "gemini-3.7-flash": ["text", "image"] },
|
|
1444
1444
|
modelReasoningEfforts: {
|
|
1445
1445
|
"gemini-3.6-flash": ["minimal", "low", "medium", "high"],
|
|
1446
1446
|
"gemini-3.5-flash": ["minimal", "low", "medium", "high"],
|
|
1447
|
+
"gemini-3.7-flash": ["minimal", "low", "medium", "high"],
|
|
1447
1448
|
"gemini-3.1-pro-preview": ["low", "medium", "high"],
|
|
1448
1449
|
},
|
|
1449
1450
|
jawcodeBundle: "google", extraMetadataAliases: ["gemini"],
|
package/src/server/index.ts
CHANGED
|
@@ -53,6 +53,7 @@ import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../p
|
|
|
53
53
|
import { providerContextCap } from "../providers/context-cap";
|
|
54
54
|
import { providerCodexAccountMode } from "../providers/registry";
|
|
55
55
|
import type { StorageCleanupPolicy } from "../types";
|
|
56
|
+
import { MAX_DECOMPRESSED_BODY_BYTES } from "./request-decompress";
|
|
56
57
|
import {
|
|
57
58
|
CodexAccountCooldownError,
|
|
58
59
|
cooldownErrorMessage,
|
|
@@ -412,6 +413,8 @@ function attachLiveSidebandUpstream(
|
|
|
412
413
|
// upstream cannot hold Codex open after response.completed; darwin no-rewrite traffic
|
|
413
414
|
// requires explicit config-eager opt-in (`auto` always stays tee on darwin).
|
|
414
415
|
// selectEagerPath(process.platform, needsClientRewrite, config.streamMode ?? "auto")
|
|
416
|
+
// Codex upstream WS runtime gating and the forced bounded single-reader branch
|
|
417
|
+
// are owned by responses/ws-upstream.ts and responses/core.ts respectively.
|
|
415
418
|
// relaySseEagerBounded(upstreamResponse.body, turnAc,
|
|
416
419
|
// new Response(eagerBody,
|
|
417
420
|
// Default shape (tee + background inspection):
|
|
@@ -721,6 +724,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
721
724
|
userCostOverlayReconciler = startUserCostOverlayReconciler({ liveConfig: config });
|
|
722
725
|
const serveOptions = {
|
|
723
726
|
idleTimeout: 255,
|
|
727
|
+
maxRequestBodySize: MAX_DECOMPRESSED_BODY_BYTES,
|
|
724
728
|
async fetch(req: Request, requestServer: Server<WsData>): Promise<Response> {
|
|
725
729
|
// The unauthenticated loopback listener (#1102) serves a fixed allowlist and nothing
|
|
726
730
|
// else. Rejecting here, before any handler runs, is what keeps the surface from growing
|
|
@@ -1181,7 +1185,6 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1181
1185
|
}
|
|
1182
1186
|
|
|
1183
1187
|
if (url.pathname === "/v1/responses" && req.method === "POST") {
|
|
1184
|
-
disableResponsesRequestTimeout(req, requestServer);
|
|
1185
1188
|
if (isDraining()) {
|
|
1186
1189
|
return drainingResponse(req, policy);
|
|
1187
1190
|
}
|
|
@@ -1210,6 +1213,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1210
1213
|
return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
|
|
1211
1214
|
const response = await handleResponses(req, config, logCtx, {
|
|
1212
1215
|
turnAdmissionLease,
|
|
1216
|
+
onRequestBodyRead: () => disableResponsesRequestTimeout(req, requestServer),
|
|
1213
1217
|
abortSignal: req.signal,
|
|
1214
1218
|
onFirstOutput: () => recordFirstOutput(logCtx, start),
|
|
1215
1219
|
onNativePassthroughTerminal: status => {
|
|
@@ -55,7 +55,7 @@ import {
|
|
|
55
55
|
type PersistedUsageEntry,
|
|
56
56
|
} from "../../usage/log";
|
|
57
57
|
import { getUsageDebugLogEntries } from "../../usage/debug";
|
|
58
|
-
import { parseRange, parseUsageSurface, summarizeUsage, type UsageRange, type UsageSummary, type UsageSurface } from "../../usage/summary";
|
|
58
|
+
import { parseRange, parseUsageSurface, rangeWindow, summarizeUsage, type UsageRange, type UsageSummary, type UsageSurface } from "../../usage/summary";
|
|
59
59
|
import { stripCodexRuntimeProviderFields } from "../../codex/auth-context";
|
|
60
60
|
import { getProviderRegistryEntry } from "../../providers/registry";
|
|
61
61
|
import { getDebugLogEntries } from "../../lib/debug-log-buffer";
|
|
@@ -87,14 +87,6 @@ import {
|
|
|
87
87
|
} from "./usage-summary-cache";
|
|
88
88
|
import { cacheApiKeyUsageFromSnapshot } from "./api-key-usage";
|
|
89
89
|
|
|
90
|
-
const USAGE_DAY_MS = 86_400_000;
|
|
91
|
-
function usageEntryMatchesSurface(entry: PersistedUsageEntry, surface: UsageSurface): boolean {
|
|
92
|
-
if (surface === "claude") return entry.surface === "claude" || entry.surface === "claude-desktop";
|
|
93
|
-
if (surface === "grok") return entry.surface === "grok";
|
|
94
|
-
if (surface === "codex") return entry.surface === undefined;
|
|
95
|
-
return true;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
90
|
function nextLocalMidnight(now: number): number {
|
|
99
91
|
const next = new Date(now);
|
|
100
92
|
next.setHours(24, 0, 0, 0);
|
|
@@ -102,24 +94,16 @@ function nextLocalMidnight(now: number): number {
|
|
|
102
94
|
}
|
|
103
95
|
|
|
104
96
|
function usageSummaryExpiresAt(
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
97
|
+
_entries: PersistedUsageEntry[],
|
|
98
|
+
_range: UsageRange,
|
|
99
|
+
_surface: UsageSurface,
|
|
108
100
|
now: number,
|
|
109
101
|
): number {
|
|
110
|
-
|
|
111
|
-
const windowMs = range === "7d" ? 7 * USAGE_DAY_MS : range === "30d" ? 30 * USAGE_DAY_MS : null;
|
|
112
|
-
if (windowMs === null) return expiresAt;
|
|
113
|
-
for (const entry of entries) {
|
|
114
|
-
if (!usageEntryMatchesSurface(entry, surface)) continue;
|
|
115
|
-
const expiry = entry.timestamp + windowMs;
|
|
116
|
-
if (expiry > now && expiry < expiresAt) expiresAt = expiry;
|
|
117
|
-
}
|
|
118
|
-
return expiresAt;
|
|
102
|
+
return nextLocalMidnight(now);
|
|
119
103
|
}
|
|
120
104
|
|
|
121
105
|
function refreshedUsageSummary<T extends UsageSummary & { historyTruncated: boolean }>(summary: T, range: UsageRange, now: number): T {
|
|
122
|
-
const since = range
|
|
106
|
+
const { since } = rangeWindow(range, now);
|
|
123
107
|
return { ...summary, since, generatedAt: now };
|
|
124
108
|
}
|
|
125
109
|
|
|
@@ -226,6 +210,7 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
|
|
|
226
210
|
&& cached.maxReadBytes === effectiveReadLimit
|
|
227
211
|
&& cached.overlayVersion === userCostOverlayVersion()
|
|
228
212
|
&& now < cached.freshUntil
|
|
213
|
+
&& now < cached.expiresAt
|
|
229
214
|
&& observedSize >= cached.lastSeenSize) {
|
|
230
215
|
return jsonResponse(refreshedUsageSummary(cached.summary, range, now));
|
|
231
216
|
}
|
|
@@ -38,6 +38,10 @@ import {
|
|
|
38
38
|
} from "../usage/debug";
|
|
39
39
|
import { matchesLogConversationId } from "./request-log-conversation";
|
|
40
40
|
import { enforceAppOwnedMemoryBudget, type RetainedStoreSnapshot } from "../lib/app-owned-memory";
|
|
41
|
+
import { capEstimateAtContextWindow } from "../lib/token-estimate";
|
|
42
|
+
import { inferCursorContextWindow } from "../adapters/cursor/discovery";
|
|
43
|
+
import { KIRO_MODEL_CONTEXT_WINDOWS, normalizeKiroModelId } from "../providers/kiro-models";
|
|
44
|
+
import { modelRecordValue } from "../reasoning-effort";
|
|
41
45
|
|
|
42
46
|
export interface RequestLogContext {
|
|
43
47
|
model: string;
|
|
@@ -839,6 +843,7 @@ export function addFinalRequestLog(
|
|
|
839
843
|
logCtx.providerAdapter ?? logCtx.provider,
|
|
840
844
|
logCtx.usage,
|
|
841
845
|
logCtx.usageLogInputTokens,
|
|
846
|
+
contextWindowForModel(logCtx.providerAdapter ?? logCtx.provider, logCtx.model),
|
|
842
847
|
);
|
|
843
848
|
const attempts = logCtx.attempts?.map(attempt => ({
|
|
844
849
|
...attempt,
|
|
@@ -960,15 +965,40 @@ interface FinalizedUsageResult {
|
|
|
960
965
|
totalTokens?: number;
|
|
961
966
|
}
|
|
962
967
|
|
|
968
|
+
/**
|
|
969
|
+
* Context window for the routed model, used to cap the token estimate (codex-router PR #140):
|
|
970
|
+
* a request the provider answered cannot have exceeded the window, so the estimate must never
|
|
971
|
+
* claim it did. The family is picked by the route ADAPTER, not the model id alone, because
|
|
972
|
+
* claude-family ids are shared between Kiro and Cursor with different windows. Kiro "auto" is
|
|
973
|
+
* a router with no fixed window and is never guessed; unknown adapters/models stay uncapped.
|
|
974
|
+
*/
|
|
975
|
+
function contextWindowForModel(adapter: string, modelId: string | undefined): number | undefined {
|
|
976
|
+
if (!modelId) return undefined;
|
|
977
|
+
if (adapter === "kiro" || adapter.startsWith("kiro-")) {
|
|
978
|
+
const normalized = normalizeKiroModelId(modelId);
|
|
979
|
+
if (normalized === "auto") return undefined;
|
|
980
|
+
return modelRecordValue(KIRO_MODEL_CONTEXT_WINDOWS, modelId)
|
|
981
|
+
?? modelRecordValue(KIRO_MODEL_CONTEXT_WINDOWS, normalized);
|
|
982
|
+
}
|
|
983
|
+
if (adapter === "cursor" || adapter.startsWith("cursor-")) {
|
|
984
|
+
return inferCursorContextWindow(modelId);
|
|
985
|
+
}
|
|
986
|
+
return undefined;
|
|
987
|
+
}
|
|
988
|
+
|
|
963
989
|
function finalizedUsage(
|
|
964
990
|
adapter: string,
|
|
965
991
|
usage: OcxUsage | undefined,
|
|
966
992
|
inputTokenEstimate: number | undefined,
|
|
993
|
+
contextWindow: number | undefined,
|
|
967
994
|
): FinalizedUsageResult {
|
|
995
|
+
// The ESTIMATE itself is capped at the model's context window (codex-router PR #140). The
|
|
996
|
+
// combined value below keeps its max(inputTokens, estimate) behavior — a provider-reported
|
|
997
|
+
// positive count is never reduced by this cap, only the estimate that could substitute it.
|
|
968
998
|
const estimate = typeof inputTokenEstimate === "number"
|
|
969
999
|
&& Number.isFinite(inputTokenEstimate)
|
|
970
1000
|
&& inputTokenEstimate >= 0
|
|
971
|
-
? inputTokenEstimate
|
|
1001
|
+
? capEstimateAtContextWindow(inputTokenEstimate, contextWindow)
|
|
972
1002
|
: undefined;
|
|
973
1003
|
const finalUsage = usageForFinalLog(adapter, usage);
|
|
974
1004
|
const usageFallback = !finalUsage && estimate !== undefined
|
|
@@ -980,7 +1010,16 @@ function finalizedUsage(
|
|
|
980
1010
|
inputTokens: Math.max(finalUsage.inputTokens, estimate),
|
|
981
1011
|
estimated: true,
|
|
982
1012
|
}
|
|
983
|
-
:
|
|
1013
|
+
: finalUsage
|
|
1014
|
+
// When the adapter alone produced an estimated count and no local estimate
|
|
1015
|
+
// exists, cap it at the context window — an adapter estimate above the window
|
|
1016
|
+
// misleads the usage dashboard. The combined branch (above) already caps the
|
|
1017
|
+
// ESTIMATE via capEstimateAtContextWindow, and Math.max preserves a real
|
|
1018
|
+
// provider-reported count, so it needs no further reduction.
|
|
1019
|
+
? (finalUsage.estimated && contextWindow !== undefined && finalUsage.inputTokens > contextWindow
|
|
1020
|
+
? { ...finalUsage, inputTokens: contextWindow }
|
|
1021
|
+
: finalUsage)
|
|
1022
|
+
: usageFallback;
|
|
984
1023
|
const totalTokens = usageTotalTokens(loggedUsage);
|
|
985
1024
|
return {
|
|
986
1025
|
status: usageStatusForFinalLog(loggedUsage),
|
|
@@ -1030,7 +1069,12 @@ export function noteAttemptSend(
|
|
|
1030
1069
|
if (typeof inputTokenEstimate === "number"
|
|
1031
1070
|
&& Number.isFinite(inputTokenEstimate)
|
|
1032
1071
|
&& inputTokenEstimate >= 0) {
|
|
1033
|
-
|
|
1072
|
+
// Store the ESTIMATE field already capped at the model's window (codex-router PR #140):
|
|
1073
|
+
// what gets persisted, and later merged into usage, never claims a count above the window.
|
|
1074
|
+
attempt.inputTokenEstimate = capEstimateAtContextWindow(
|
|
1075
|
+
inputTokenEstimate,
|
|
1076
|
+
contextWindowForModel(attempt.adapter, attempt.model),
|
|
1077
|
+
);
|
|
1034
1078
|
}
|
|
1035
1079
|
if (recovery && !attempt.recoveryKinds.includes(recovery)) {
|
|
1036
1080
|
attempt.recoveryKinds.push(recovery);
|
|
@@ -1047,6 +1091,7 @@ export function finishRequestAttempt(
|
|
|
1047
1091
|
attempt.adapter,
|
|
1048
1092
|
usage ?? attempt.usage,
|
|
1049
1093
|
attempt.inputTokenEstimate,
|
|
1094
|
+
contextWindowForModel(attempt.adapter, attempt.model),
|
|
1050
1095
|
);
|
|
1051
1096
|
attempt.status = status;
|
|
1052
1097
|
attempt.durationMs = Math.max(0, durationMs);
|