@bitkyc08/opencodex 2.48.0 → 2.50.0

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 (141) hide show
  1. package/AGENTS_INSTALL.md +9 -1
  2. package/README.md +11 -5
  3. package/SPONSORS.md +1 -1
  4. package/assets/sponsors/orcarouter.png +0 -0
  5. package/assets/sponsors/packycode.png +0 -0
  6. package/gui/dist/assets/index-BoBRSehJ.css +1 -0
  7. package/gui/dist/assets/index-C39tnjXO.js +115 -0
  8. package/gui/dist/index.html +2 -2
  9. package/gui/dist/provider-icons/packycode.svg +19 -0
  10. package/gui/dist/provider-icons/qoder.svg +5 -0
  11. package/package.json +5 -3
  12. package/src/adapters/anthropic.ts +31 -16
  13. package/src/adapters/codebuddy/adapter.ts +85 -0
  14. package/src/adapters/codebuddy/profiles.ts +52 -0
  15. package/src/adapters/coding-agent/profile.ts +100 -0
  16. package/src/adapters/coding-agent/protocol.ts +463 -0
  17. package/src/adapters/coding-agent/turn.ts +353 -0
  18. package/src/adapters/google.ts +15 -11
  19. package/src/adapters/mimo-free.ts +3 -0
  20. package/src/adapters/openai-chat.ts +2 -2
  21. package/src/adapters/openai-responses.ts +18 -11
  22. package/src/adapters/qoder/adapter.ts +70 -0
  23. package/src/adapters/qoder/live-models.ts +89 -0
  24. package/src/adapters/qoder/profiles.ts +36 -0
  25. package/src/adapters/registry.ts +12 -0
  26. package/src/adapters/responses-tool-schema.ts +113 -8
  27. package/src/claude/inbound.ts +17 -5
  28. package/src/cli/account-api.ts +18 -3
  29. package/src/cli/account-auth.ts +8 -1
  30. package/src/cli/account-extended.ts +2 -1
  31. package/src/cli/account.ts +1 -0
  32. package/src/cli/capabilities.ts +15 -1
  33. package/src/cli/dispatch.ts +2 -0
  34. package/src/cli/doctor.ts +40 -0
  35. package/src/cli/effort.ts +24 -8
  36. package/src/cli/help.ts +2 -0
  37. package/src/cli/index.ts +29 -2
  38. package/src/cli/models-runtime.ts +8 -3
  39. package/src/cli/observe.ts +13 -3
  40. package/src/cli/provider-runtime.ts +2 -1
  41. package/src/cli/registry.ts +2 -2
  42. package/src/cli/system-command.ts +10 -3
  43. package/src/cli/usage-report.ts +9 -5
  44. package/src/clients/config-export/zcode.ts +24 -0
  45. package/src/codex/account-lifecycle.ts +35 -2
  46. package/src/codex/account-runtime-state.ts +6 -1
  47. package/src/codex/account-store.ts +72 -9
  48. package/src/codex/account-usability.ts +3 -2
  49. package/src/codex/auth-api.ts +113 -26
  50. package/src/codex/auth-collision.ts +12 -2
  51. package/src/codex/auth-context.ts +96 -7
  52. package/src/codex/catalog/parsing.ts +23 -0
  53. package/src/codex/catalog/provider-fetch.ts +144 -11
  54. package/src/codex/catalog/sync.ts +14 -0
  55. package/src/codex/inject.ts +128 -30
  56. package/src/codex/internal/catalog-writer.ts +3 -0
  57. package/src/codex/journal.ts +61 -12
  58. package/src/codex/model-cache.ts +11 -4
  59. package/src/codex/native-profile-startup.ts +72 -5
  60. package/src/codex/native-profile-store.ts +2 -2
  61. package/src/codex/ocx-compaction-history.ts +226 -0
  62. package/src/codex/project-config-warnings.ts +3 -1
  63. package/src/codex/quota-auto-refresh.ts +6 -1
  64. package/src/codex/quota.ts +71 -15
  65. package/src/codex/reserve-availability.ts +21 -5
  66. package/src/codex/runtime.ts +45 -1
  67. package/src/codex/sync.ts +5 -0
  68. package/src/combos/index.ts +2 -0
  69. package/src/combos/resolve.ts +52 -0
  70. package/src/config.ts +59 -0
  71. package/src/generated/compatibility-version.json +178 -114
  72. package/src/images/loop.ts +1 -0
  73. package/src/images/xai-video-client.ts +2 -0
  74. package/src/integrations/registry.ts +1 -0
  75. package/src/lib/errors.ts +8 -0
  76. package/src/lib/privacy.ts +25 -0
  77. package/src/lib/process-control.ts +52 -8
  78. package/src/lib/upstream-retry.ts +1 -0
  79. package/src/oauth/chatgpt.ts +83 -0
  80. package/src/oauth/health.ts +47 -12
  81. package/src/oauth/index.ts +46 -8
  82. package/src/oauth/token-guardian.ts +32 -6
  83. package/src/oauth/xai.ts +151 -8
  84. package/src/providers/api-key-selection-capture.ts +10 -0
  85. package/src/providers/api-key-selection.ts +2 -7
  86. package/src/providers/caller-authorization.ts +36 -0
  87. package/src/providers/codebuddy-models.ts +184 -0
  88. package/src/providers/derive.ts +5 -0
  89. package/src/providers/free-directory.ts +26 -2
  90. package/src/providers/google-ai-studio-model-discovery.ts +74 -0
  91. package/src/providers/openai-sidecar.ts +35 -11
  92. package/src/providers/opencode-zen-rate-limit.ts +75 -0
  93. package/src/providers/qoder-models.ts +25 -0
  94. package/src/providers/quota.ts +15 -0
  95. package/src/providers/registry.ts +140 -1
  96. package/src/responses/compaction.ts +4 -0
  97. package/src/responses/task-input.ts +21 -1
  98. package/src/router.ts +1 -1
  99. package/src/server/auth-cors.ts +6 -0
  100. package/src/server/chat-completions.ts +30 -13
  101. package/src/server/chat-native.ts +10 -1
  102. package/src/server/claude-messages.ts +17 -7
  103. package/src/server/images.ts +3 -2
  104. package/src/server/index.ts +25 -2
  105. package/src/server/management/account-selection-stream.ts +13 -4
  106. package/src/server/management/config-routes.ts +24 -5
  107. package/src/server/management/logs-usage-routes.ts +5 -1
  108. package/src/server/management/model-rows.ts +16 -1
  109. package/src/server/management/native-integration-routes.ts +2 -1
  110. package/src/server/management/oauth-account-routes.ts +6 -2
  111. package/src/server/management/provider-routes.ts +33 -2
  112. package/src/server/management/request-history-routes.ts +4 -2
  113. package/src/server/management/route-registry.ts +5 -4
  114. package/src/server/management/shared.ts +66 -3
  115. package/src/server/management-api.ts +15 -1
  116. package/src/server/port-reclaim.ts +11 -26
  117. package/src/server/request-decompress.ts +91 -3
  118. package/src/server/request-log.ts +16 -0
  119. package/src/server/responses/codex-ws-wire.ts +1 -1
  120. package/src/server/responses/collaboration.ts +4 -9
  121. package/src/server/responses/compact.ts +8 -2
  122. package/src/server/responses/context-overflow.ts +11 -0
  123. package/src/server/responses/core.ts +285 -57
  124. package/src/server/responses/fetch-helpers.ts +18 -7
  125. package/src/server/responses/policy-fallback.ts +18 -2
  126. package/src/server/search.ts +2 -2
  127. package/src/service.ts +128 -9
  128. package/src/storage/cleanup.ts +77 -45
  129. package/src/types/accounts.ts +18 -0
  130. package/src/types/config.ts +43 -1
  131. package/src/types/provider.ts +56 -0
  132. package/src/types.ts +4 -0
  133. package/src/usage/log.ts +24 -0
  134. package/src/vision/anthropic-describe.ts +1 -0
  135. package/src/web-search/anthropic-executor.ts +1 -0
  136. package/src/web-search/loop.ts +1 -0
  137. package/src/web-search/ollama-executor.ts +127 -0
  138. package/src/web-search/passthrough-bridge.ts +761 -0
  139. package/src/web-search/progress-stream.ts +4 -0
  140. package/gui/dist/assets/index-B5r7LNHN.js +0 -115
  141. package/gui/dist/assets/index-D5SiRo8X.css +0 -1
package/src/oauth/xai.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  /** xAI OAuth flow (Grok account login). Ported from jawcode oauth/xai.ts. */
2
+ import { abortError, sleepWithAbort } from "../lib/upstream-retry";
2
3
  import { OAuthCallbackFlow, type OAuthCallbackFlowOptions } from "./callback-server";
3
4
  import { generatePKCE } from "./pkce";
4
5
  import type { LocalTokenImportMode, OAuthController, OAuthCredentials } from "./types";
@@ -11,6 +12,9 @@ const XAI_OAUTH_CALLBACK_PORT = 56121;
11
12
  const XAI_OAUTH_CALLBACK_PATH = "/callback";
12
13
  const XAI_OAUTH_REFRESH_SKEW_MS = 2 * 60 * 1000;
13
14
  const TOKEN_REQUEST_TIMEOUT_MS = 30_000;
15
+ const RETRY_AFTER_MAX_DELAY_MS = 60_000;
16
+ const JITTER_DELAY_CAP_MS = 2_000;
17
+ const XAI_TRUSTED_AUTH_HOSTS = new Set(["auth.x.ai", "accounts.x.ai"]);
14
18
 
15
19
  export const XAI_LOCAL_CLI_DETACH_WARNING =
16
20
  "[oauth:xai] Grok CLI credential was stale; refreshed into OpenCodex ownership. Grok CLI may require login again.";
@@ -45,10 +49,21 @@ function requestSignal(signal: AbortSignal | undefined): AbortSignal {
45
49
  }
46
50
 
47
51
  function validateXaiEndpoint(rawUrl: string): string {
48
- const parsed = new URL(rawUrl);
52
+ let parsed: URL;
53
+ try {
54
+ parsed = new URL(rawUrl);
55
+ } catch {
56
+ throw new Error("xAI OAuth discovery returned an unparseable endpoint URL");
57
+ }
49
58
  const host = parsed.hostname.toLowerCase();
50
- if (parsed.protocol !== "https:" || (host !== "x.ai" && !host.endsWith(".x.ai"))) {
51
- throw new Error(`xAI OAuth discovery returned an unexpected endpoint: ${rawUrl}`);
59
+ if (
60
+ parsed.protocol !== "https:"
61
+ || parsed.username !== ""
62
+ || parsed.password !== ""
63
+ || parsed.port !== ""
64
+ || !XAI_TRUSTED_AUTH_HOSTS.has(host)
65
+ ) {
66
+ throw new Error(`xAI OAuth discovery returned an unexpected endpoint (host: ${host || "none"})`);
52
67
  }
53
68
  return parsed.toString();
54
69
  }
@@ -94,15 +109,135 @@ function getTokenIdentity(accessToken: string, idToken: string | undefined): { a
94
109
 
95
110
  export class XaiTokenRequestError extends Error { constructor(public readonly status?:number,public readonly oauthError?:string,message="xAI token request failed",options?:{cause?:unknown}){super(message,options);this.name="XaiTokenRequestError";} }
96
111
  export interface XaiTokenRetryDeps { sleep?:(ms:number)=>Promise<void>; random?:()=>number }
97
- function isAbortError(error:unknown):boolean{return error instanceof DOMException&&error.name==="AbortError";}
98
- function retryDelay(attempt:number,retryAfter:string|null,random:()=>number):number{const base=attempt===1?100:250,j=Math.round(base*(.75+random()*.5)),seconds=retryAfter!==null&&/^\d+$/.test(retryAfter)?Number(retryAfter):0;return Math.min(2000,Math.max(j,seconds*1000));}
99
- async function readTokenError(response:Response):Promise<XaiTokenRequestError>{let oauthError:string|undefined,detail="";try{const body=await response.json() as {error?:unknown;error_description?:unknown};if(typeof body.error==="string")oauthError=body.error;if(typeof body.error_description==="string")detail=body.error_description;}catch{}const suffix=detail?`: ${detail}`:oauthError?`: ${oauthError}`:"";return new XaiTokenRequestError(response.status,oauthError,`xAI token request failed: ${response.status}${suffix}`);}
112
+ const IMF_FIXDATE_RE = /^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), (\d{2}) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (\d{4}) (\d{2}):(\d{2}):(\d{2}) GMT$/i;
113
+ const RFC850_DATE_RE = /^(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (\d{2})-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-(\d{2}) (\d{2}):(\d{2}):(\d{2}) GMT$/i;
114
+ const ASCTIME_DATE_RE = /^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) ( \d|\d{2}) (\d{2}):(\d{2}):(\d{2}) (\d{4})$/i;
115
+ const HTTP_MONTH_INDEX: Record<string, number> = {
116
+ jan: 0, feb: 1, mar: 2, apr: 3, may: 4, jun: 5,
117
+ jul: 6, aug: 7, sep: 8, oct: 9, nov: 10, dec: 11,
118
+ };
119
+
120
+ function parseUtcDateParts(
121
+ year: number,
122
+ monthName: string,
123
+ day: number,
124
+ hour: number,
125
+ minute: number,
126
+ second: number,
127
+ ): number | undefined {
128
+ const month = HTTP_MONTH_INDEX[monthName.toLowerCase()];
129
+ if (month === undefined) return undefined;
130
+ const timestamp = Date.UTC(year, month, day, hour, minute, second);
131
+ const parsed = new Date(timestamp);
132
+ return parsed.getUTCFullYear() === year
133
+ && parsed.getUTCMonth() === month
134
+ && parsed.getUTCDate() === day
135
+ && parsed.getUTCHours() === hour
136
+ && parsed.getUTCMinutes() === minute
137
+ && parsed.getUTCSeconds() === second
138
+ ? timestamp
139
+ : undefined;
140
+ }
141
+
142
+ function parseHttpDateMs(value: string, now: number): number | undefined {
143
+ const match = IMF_FIXDATE_RE.exec(value);
144
+ if (match) {
145
+ return parseUtcDateParts(
146
+ Number(match[3]), match[2]!, Number(match[1]),
147
+ Number(match[4]), Number(match[5]), Number(match[6]),
148
+ );
149
+ }
150
+ const rfc850 = RFC850_DATE_RE.exec(value);
151
+ if (rfc850) {
152
+ // Two-digit years more than 50 years in the future are in the past (RFC 9110).
153
+ const currentYear = new Date(now).getUTCFullYear();
154
+ let year = Math.floor(currentYear / 100) * 100 + Number(rfc850[3]);
155
+ const candidateTimeOfYear = Date.UTC(
156
+ 2000, HTTP_MONTH_INDEX[rfc850[2]!.toLowerCase()]!, Number(rfc850[1]),
157
+ Number(rfc850[4]), Number(rfc850[5]), Number(rfc850[6]),
158
+ );
159
+ const current = new Date(now);
160
+ const currentTimeOfYear = Date.UTC(
161
+ 2000, current.getUTCMonth(), current.getUTCDate(),
162
+ current.getUTCHours(), current.getUTCMinutes(), current.getUTCSeconds(),
163
+ current.getUTCMilliseconds(),
164
+ );
165
+ const yearDelta = year - currentYear;
166
+ if (yearDelta < -50 || (yearDelta === -50 && candidateTimeOfYear < currentTimeOfYear)) {
167
+ year += 100;
168
+ } else if (yearDelta > 50 || (yearDelta === 50 && candidateTimeOfYear > currentTimeOfYear)) {
169
+ year -= 100;
170
+ }
171
+ return parseUtcDateParts(
172
+ year, rfc850[2]!, Number(rfc850[1]),
173
+ Number(rfc850[4]), Number(rfc850[5]), Number(rfc850[6]),
174
+ );
175
+ }
176
+ const asctime = ASCTIME_DATE_RE.exec(value);
177
+ if (!asctime) return undefined;
178
+ return parseUtcDateParts(
179
+ Number(asctime[6]), asctime[1]!, Number(asctime[2]),
180
+ Number(asctime[3]), Number(asctime[4]), Number(asctime[5]),
181
+ );
182
+ }
183
+
184
+ function parseRetryAfterMs(retryAfter: string | null): number | undefined {
185
+ const text = retryAfter?.trim();
186
+ if (!text) return undefined;
187
+ if (/^\d+(?:\.\d+)?$/.test(text)) {
188
+ const ms = Math.ceil(Number(text) * 1000);
189
+ return ms > 0 ? ms : undefined;
190
+ }
191
+ const now = Date.now();
192
+ const timestamp = parseHttpDateMs(text, now);
193
+ if (timestamp === undefined) return undefined;
194
+ const delay = timestamp - now;
195
+ return delay > 0 ? delay : undefined;
196
+ }
197
+
198
+ function jitterDelay(attempt: number, random: () => number): number {
199
+ const base = attempt === 1 ? 100 : 250;
200
+ return Math.min(JITTER_DELAY_CAP_MS, Math.round(base * (0.75 + random() * 0.5)));
201
+ }
202
+
203
+ /**
204
+ * Delay before the next attempt, or undefined when the server asked for a wait
205
+ * beyond the retry budget — retrying earlier than Retry-After would hammer the
206
+ * token endpoint, so the caller fails the request instead of clamping.
207
+ */
208
+ function retryDelay(attempt: number, retryAfter: string | null, random: () => number): number | undefined {
209
+ const serverMs = parseRetryAfterMs(retryAfter);
210
+ if (serverMs === undefined) return jitterDelay(attempt, random);
211
+ return serverMs <= RETRY_AFTER_MAX_DELAY_MS ? serverMs : undefined;
212
+ }
213
+
214
+ async function sleepAbortable(
215
+ ms: number,
216
+ sleep: (ms: number) => Promise<void>,
217
+ signal: AbortSignal | undefined,
218
+ ): Promise<void> {
219
+ if (!signal) return sleep(ms);
220
+ if (signal.aborted) throw abortError(signal);
221
+ let onAbort!: () => void;
222
+ try {
223
+ await Promise.race([
224
+ sleep(ms),
225
+ new Promise<never>((_, reject) => {
226
+ onAbort = () => reject(abortError(signal));
227
+ signal.addEventListener("abort", onAbort, { once: true });
228
+ }),
229
+ ]);
230
+ } finally {
231
+ signal.removeEventListener("abort", onAbort);
232
+ }
233
+ }
234
+ async function readTokenError(response:Response):Promise<XaiTokenRequestError>{let oauthError:string|undefined,detail="";try{const body=await response.json() as {error?:unknown;error_description?:unknown};if(typeof body.error==="string")oauthError=body.error;if(typeof body.error_description==="string")detail=body.error_description;}catch{/* non-JSON error body: fall through to the generic message */}const suffix=detail?`: ${detail}`:oauthError?`: ${oauthError}`:"";return new XaiTokenRequestError(response.status,oauthError,`xAI token request failed: ${response.status}${suffix}`);}
100
235
  export async function postXaiToken(
101
236
  tokenEndpoint: string,
102
237
  body: Record<string, string>,
103
238
  signal?: AbortSignal, deps:XaiTokenRetryDeps={},
104
239
  ): Promise<XaiTokenPayload> {
105
- const sleep=deps.sleep??(ms=>Bun.sleep(ms)),random=deps.random??Math.random;let last:unknown;
240
+ const sleep=deps.sleep??((ms:number)=>sleepWithAbort(ms,signal)),random=deps.random??Math.random;let last:unknown;
106
241
  for(let attempt=1;attempt<=3;attempt++){let response:Response;try{response=await fetch(tokenEndpoint, {
107
242
  method: "POST",
108
243
  headers: {
@@ -111,7 +246,15 @@ export async function postXaiToken(
111
246
  },
112
247
  body: new URLSearchParams(body).toString(),
113
248
  signal: requestSignal(signal),
114
- });}catch(error){if(isAbortError(error)&&signal?.aborted)throw error;last=error;if(attempt===3)throw new XaiTokenRequestError(undefined,undefined,"xAI token request failed: network error",{cause:error});await sleep(retryDelay(attempt,null,random));continue;}if(response.ok)return await response.json() as XaiTokenPayload;const error=await readTokenError(response);last=error;if(!(response.status===429||response.status>=500)||attempt===3)throw error;await sleep(retryDelay(attempt,response.headers.get("retry-after"),random));}throw last;
249
+ });}catch(error){
250
+ if(signal?.aborted)throw error;
251
+ const name=(error as {name?:string}|undefined)?.name;
252
+ if(name==="AbortError"||name==="TimeoutError")throw error;
253
+ last=error;
254
+ if(attempt===3)throw new XaiTokenRequestError(undefined,undefined,"xAI token request failed: network error",{cause:error});
255
+ await sleepAbortable(jitterDelay(attempt,random),sleep,signal);
256
+ continue;
257
+ }if(response.ok)return await response.json() as XaiTokenPayload;const error=await readTokenError(response);last=error;if(!(response.status===429||response.status>=500)||attempt===3)throw error;if(signal?.aborted)throw error;const delay=retryDelay(attempt,response.headers.get("retry-after"),random);if(delay===undefined)throw error;await sleepAbortable(delay,sleep,signal);}throw last;
115
258
  }
116
259
 
117
260
  function credentialsFromTokenPayload(payload: XaiTokenPayload, refreshFallback = ""): OAuthCredentials {
@@ -0,0 +1,10 @@
1
+ import type { OcxProviderConfig } from "../types";
2
+ import type { ProviderApiKeySelection } from "../types/provider";
3
+
4
+ export function captureProviderApiKeySelection(provider: OcxProviderConfig): ProviderApiKeySelection {
5
+ return {
6
+ entryId: provider.apiKeyPool?.find(entry => entry.key === provider.apiKey)?.id,
7
+ reference: provider.apiKey,
8
+ revision: provider.apiKeySelectionRevision,
9
+ };
10
+ }
@@ -6,14 +6,9 @@ import type { ProviderApiKeySelection } from "../types/provider";
6
6
  import { routedProviderConfig } from "../router";
7
7
  import { OPENCODE_GO_SESSION_HEADER } from "./opencode-go-transport";
8
8
  import { resolveProviderTransport, XAI_GROK_COMPATIBILITY, type OcxProviderTransport } from "./xai-transport";
9
+ import { captureProviderApiKeySelection } from "./api-key-selection-capture";
9
10
 
10
- export function captureProviderApiKeySelection(provider: OcxProviderConfig): ProviderApiKeySelection {
11
- return {
12
- entryId: provider.apiKeyPool?.find(entry => entry.key === provider.apiKey)?.id,
13
- reference: provider.apiKey,
14
- revision: provider.apiKeySelectionRevision,
15
- };
16
- }
11
+ export { captureProviderApiKeySelection } from "./api-key-selection-capture";
17
12
 
18
13
  function matchesSelection(provider: OcxProviderConfig, expected: ProviderApiKeySelection): boolean {
19
14
  const current = captureProviderApiKeySelection(provider);
@@ -0,0 +1,36 @@
1
+ import type { OcxConfig, OcxProviderConfig } from "../types";
2
+ import { inspectChatGptDomainClaim } from "../oauth/chatgpt";
3
+ import { isProxyAdmissionSecret } from "../server/auth-cors";
4
+ import { isCanonicalOpenAiForwardProvider } from "./openai-tiers";
5
+
6
+ /** The caller's own ChatGPT-domain credential as plain Direct forwarding would use it. */
7
+ export type CallerDirectAuth = Readonly<{ authorization: string; chatgptAccountId?: string }>;
8
+
9
+ /** Whether this transport can consume the request's Authorization as its upstream credential. */
10
+ export function providerConsumesCallerAuthorization(provider: OcxProviderConfig): boolean {
11
+ return isCanonicalOpenAiForwardProvider(provider)
12
+ || (provider.adapter === "cursor" && provider.authMode !== "oauth" && !provider.apiKey?.trim());
13
+ }
14
+
15
+ /**
16
+ * Capture the caller's Direct credential for a canonical-route restore after an internal
17
+ * rewrite. This restore is intentionally STRICTER than plain unchanged-route Direct
18
+ * forwarding: only a bearer with a VALID ChatGPT-domain claim qualifies (a clean single
19
+ * non-proxy JWT whose ChatGPT-specific account marker is well-formed and unambiguous, with
20
+ * any explicit account header matching it). An opaque bearer, a foreign JWT carrying only a
21
+ * generic organizations claim, and a ChatGPT-marked but malformed/conflicting token are all
22
+ * rejected: after a shadow/thread rewrite a self-asserted header cannot distinguish a
23
+ * caller-owned main credential from a foreign source-route token, so those cases stay
24
+ * fail-closed. Claims are decoded locally as routing markers, not authenticity proof, and
25
+ * unchanged-route Direct forwarding is governed by its own legacy rules.
26
+ */
27
+ export function captureCallerDirectAuth(incomingHeaders: Headers, config: OcxConfig): CallerDirectAuth | null {
28
+ const raw = incomingHeaders.get("authorization")?.trim();
29
+ const bearer = /^Bearer[\t ]+([^\s,]+)$/i.exec(raw ?? "")?.[1];
30
+ if (!bearer || isProxyAdmissionSecret(bearer, config)) return null;
31
+ const claim = inspectChatGptDomainClaim(bearer);
32
+ if (claim.kind !== "valid") return null;
33
+ const headerAccount = incomingHeaders.get("chatgpt-account-id")?.trim();
34
+ if (headerAccount && headerAccount !== claim.accountId) return null;
35
+ return { authorization: `Bearer ${bearer}`, chatgptAccountId: claim.accountId };
36
+ }
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Curated CodeBuddy model catalogs, transcribed from the OFFICIAL model manifest bundled with the
3
+ * vendor CLI (`@tencent-ai/codebuddy-code` v2.143.0: `product.json` for the global/`public`
4
+ * environment, `product.internal.json` for the China/`internal` environment) and cross-checked
5
+ * against the CLI's own `--model` accept-list. Verified 2026-09-03.
6
+ *
7
+ * Global and CN are deliberately NOT the same roster (§八). Context windows, output caps, vision
8
+ * and reasoning ladders are filled ONLY where the official manifest states them; a model with no
9
+ * published figure is omitted rather than guessed (§二十八/§二十九). CodeBuddy exposes no documented
10
+ * third-party live `/v1/models` endpoint, so these providers seed a static catalog
11
+ * (`liveModels: false`) exactly like the Kiro and Command Code entries.
12
+ */
13
+
14
+ /** Global (`public`) session models accepted by `codebuddy --model`. */
15
+ export const CODEBUDDY_GLOBAL_MODELS = [
16
+ "default-model",
17
+ "fast-model",
18
+ "balanced-model",
19
+ "primary-model",
20
+ "deep-model",
21
+ "gpt-5.6-sol",
22
+ "gpt-5.6-terra",
23
+ "gpt-5.6-luna",
24
+ "gpt-5.5",
25
+ "gpt-5.4",
26
+ "gpt-5.3-codex",
27
+ "gemini-3.5-flash",
28
+ "glm-5.3",
29
+ "glm-5.2",
30
+ "kimi-k3",
31
+ "kimi-k2.6",
32
+ "minimax-m3",
33
+ ];
34
+
35
+ /** China (`internal`) session models from the official internal manifest (text/chat models only). */
36
+ export const CODEBUDDY_CN_MODELS = [
37
+ "default",
38
+ "deepseek-v4-pro",
39
+ "deepseek-v4-flash",
40
+ "minimax-m3",
41
+ "minimax-m2.7",
42
+ "glm-5.2",
43
+ "glm-5.1",
44
+ "glm-5.0",
45
+ "glm-5.0-turbo",
46
+ "glm-5v-turbo",
47
+ "glm-4.7",
48
+ "kimi-k3-1",
49
+ "kimi-k2.7",
50
+ "kimi-k2.6",
51
+ "kimi-k2.5",
52
+ "deepseek-v3-2-volc",
53
+ "hy3",
54
+ "hunyuan-chat",
55
+ ];
56
+
57
+ /**
58
+ * The CLI documents a single `--effort` ladder (minimal, low, medium, high, xhigh, max). The Codex
59
+ * reasoning ladder overlaps it at low..max; `minimal`/`none` are Codex sentinels normalized by
60
+ * `mapReasoningEffort`, and `ultra` folds to `max`. Declared provider-wide, then narrowed per model
61
+ * where the official manifest publishes a smaller `supportedEfforts`.
62
+ */
63
+ export const CODEBUDDY_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
64
+
65
+ export const CODEBUDDY_GLOBAL_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
66
+ "default-model": 176_000,
67
+ "fast-model": 200_000,
68
+ "balanced-model": 256_000,
69
+ "primary-model": 272_000,
70
+ "deep-model": 176_000,
71
+ "gpt-5.6-sol": 1_000_000,
72
+ "gpt-5.6-terra": 1_000_000,
73
+ "gpt-5.6-luna": 1_000_000,
74
+ "gpt-5.5": 1_000_000,
75
+ "gpt-5.4": 272_000,
76
+ "gpt-5.3-codex": 272_000,
77
+ "gemini-3.5-flash": 1_000_000,
78
+ "glm-5.3": 1_000_000,
79
+ "glm-5.2": 1_000_000,
80
+ "kimi-k3": 1_000_000,
81
+ "kimi-k2.6": 256_000,
82
+ "minimax-m3": 512_000,
83
+ };
84
+
85
+ export const CODEBUDDY_GLOBAL_MODEL_MAX_OUTPUT_TOKENS: Record<string, number> = {
86
+ "default-model": 24_000,
87
+ "fast-model": 32_000,
88
+ "balanced-model": 32_000,
89
+ "primary-model": 72_000,
90
+ "deep-model": 24_000,
91
+ "gpt-5.6-sol": 128_000,
92
+ "gpt-5.6-terra": 128_000,
93
+ "gpt-5.6-luna": 128_000,
94
+ "gpt-5.5": 72_000,
95
+ "gpt-5.4": 128_000,
96
+ "gpt-5.3-codex": 128_000,
97
+ "gemini-3.5-flash": 65_536,
98
+ "glm-5.3": 48_000,
99
+ "glm-5.2": 48_000,
100
+ "kimi-k3": 32_000,
101
+ "kimi-k2.6": 32_000,
102
+ "minimax-m3": 128_000,
103
+ };
104
+
105
+ /** Per-model ladders narrowed from the official manifest's `reasoning.supportedEfforts`. */
106
+ export const CODEBUDDY_GLOBAL_MODEL_REASONING_EFFORTS: Record<string, string[]> = {
107
+ "gpt-5.6-sol": ["low", "medium", "high", "xhigh"],
108
+ "gpt-5.6-terra": ["low", "medium", "high", "xhigh"],
109
+ "gpt-5.6-luna": ["low", "medium", "high", "xhigh"],
110
+ "glm-5.3": ["low", "high", "max"],
111
+ "glm-5.2": ["high", "xhigh"],
112
+ };
113
+
114
+ export const CODEBUDDY_GLOBAL_MODEL_DEFAULT_REASONING_EFFORTS: Record<string, string> = {
115
+ "gpt-5.6-sol": "high",
116
+ "gpt-5.6-terra": "high",
117
+ "gpt-5.6-luna": "high",
118
+ "glm-5.3": "high",
119
+ "glm-5.2": "high",
120
+ };
121
+
122
+ export const CODEBUDDY_CN_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
123
+ "default": 200_000,
124
+ "deepseek-v4-pro": 1_000_000,
125
+ "deepseek-v4-flash": 1_000_000,
126
+ "minimax-m3": 512_000,
127
+ "minimax-m2.7": 200_000,
128
+ "glm-5.2": 1_000_000,
129
+ "glm-5.1": 200_000,
130
+ "glm-5.0": 200_000,
131
+ "glm-5.0-turbo": 200_000,
132
+ "glm-5v-turbo": 200_000,
133
+ "glm-4.7": 200_000,
134
+ "kimi-k3-1": 1_000_000,
135
+ "kimi-k2.7": 256_000,
136
+ "kimi-k2.6": 256_000,
137
+ "kimi-k2.5": 164_000,
138
+ "deepseek-v3-2-volc": 96_000,
139
+ "hy3": 192_000,
140
+ "hunyuan-chat": 200_000,
141
+ };
142
+
143
+ export const CODEBUDDY_CN_MODEL_MAX_OUTPUT_TOKENS: Record<string, number> = {
144
+ "default": 24_000,
145
+ "deepseek-v4-pro": 50_000,
146
+ "deepseek-v4-flash": 50_000,
147
+ "minimax-m3": 128_000,
148
+ "minimax-m2.7": 48_000,
149
+ "glm-5.2": 48_000,
150
+ "glm-5.1": 48_000,
151
+ "glm-5.0": 48_000,
152
+ "glm-5.0-turbo": 48_000,
153
+ "glm-5v-turbo": 64_000,
154
+ "glm-4.7": 48_000,
155
+ "kimi-k3-1": 32_000,
156
+ "kimi-k2.7": 32_000,
157
+ "kimi-k2.6": 32_000,
158
+ "kimi-k2.5": 32_000,
159
+ "deepseek-v3-2-volc": 32_000,
160
+ "hy3": 64_000,
161
+ "hunyuan-chat": 8_192,
162
+ };
163
+
164
+ export const CODEBUDDY_CN_MODEL_REASONING_EFFORTS: Record<string, string[]> = {
165
+ "hy3": ["low", "high"],
166
+ };
167
+
168
+ export const CODEBUDDY_CN_MODEL_DEFAULT_REASONING_EFFORTS: Record<string, string> = {
169
+ "hy3": "high",
170
+ };
171
+
172
+ /**
173
+ * Text-only models (official manifest `supportsImages: false`). Images for any OTHER model are
174
+ * passed through natively; a model listed here has its images routed through the proxy's vision
175
+ * sidecar rather than being silently dropped (§二十九).
176
+ */
177
+ export const CODEBUDDY_CN_NO_VISION_MODELS = [
178
+ "default",
179
+ "glm-5.0",
180
+ "glm-5.0-turbo",
181
+ "glm-4.7",
182
+ "deepseek-v3-2-volc",
183
+ "hunyuan-chat",
184
+ ];
@@ -80,6 +80,10 @@ export interface DerivedProviderPreset {
80
80
  keyOptional?: boolean;
81
81
  /** Free pricing (may still require a key). */
82
82
  freeTier?: boolean;
83
+ /** Sponsor tier from SPONSORS.md; the picker pins and labels these rows. */
84
+ sponsor?: "main" | "standard";
85
+ /** Sponsor landing URL (with its tracking parameters), for the picker's row link. */
86
+ sponsorUrl?: string;
83
87
  /**
84
88
  * Endpoint picker rows (token plan / payg / custom). When present, the add-provider
85
89
  * form shows a dropdown; `custom` reveals a free-text base URL field.
@@ -607,6 +611,7 @@ function entryToPreset(entry: ProviderRegistryEntry): DerivedProviderPreset {
607
611
  ...(entry.note ? { note: entry.note } : {}),
608
612
  ...(entry.keyOptional ? { keyOptional: true } : {}),
609
613
  ...(entry.freeTier ? { freeTier: true } : {}),
614
+ ...(entry.sponsor ? { sponsor: entry.sponsor.tier, sponsorUrl: entry.sponsor.url } : {}),
610
615
  ...(entry.baseUrlChoices ? { baseUrlChoices: entry.baseUrlChoices.map(c => ({ ...c })) } : {}),
611
616
  };
612
617
  }
@@ -19,7 +19,7 @@ export const FREE_PROVIDER_ACCESS_GROUPS = {
19
19
  "recurring-credit": ["bytez", "nous-research"],
20
20
  "signup-credit": [
21
21
  "agentrouter", "ai21", "baichuan", "baseten", "deepinfra", "deepseek", "doubao", "fireworks", "freemodel-dev", "glm-cn",
22
- "hyperbolic", "longcat", "monsterapi", "nebius", "novita", "nscale", "nvidia", "predibase", "publicai", "qoder",
22
+ "hyperbolic", "longcat", "monsterapi", "nebius", "novita", "nscale", "nvidia", "predibase", "publicai", "qoder", "qoder-cn",
23
23
  "scaleway", "sensenova", "stepfun", "together", "vertex",
24
24
  ],
25
25
  } as const satisfies Record<ProviderAccessGroup, readonly string[]>;
@@ -140,6 +140,30 @@ const CONNECTABLE: Record<string, ConnectableOverride> = {
140
140
  nscale: openAi("https://inference.api.nscale.com/v1", "https://console.nscale.com", { supportLevel: "supported", verification: "official", documentationUrl: "https://docs.nscale.com/docs/use-cases/chat", modelsUrl: "https://inference.api.nscale.com/v1/models", lastVerified: "2026-08-03" }),
141
141
  nvidia: openAi("https://integrate.api.nvidia.com/v1", "https://build.nvidia.com", { supportLevel: "supported", verification: "official", documentationUrl: "https://docs.api.nvidia.com/nim/reference/llm-apis" }),
142
142
  publicai: openAi("https://api.publicai.co/v1", "https://publicai.co"),
143
+ qoder: {
144
+ baseUrl: "https://qoder.com",
145
+ dashboardUrl: "https://qoder.com/account/integrations",
146
+ adapter: "qoder",
147
+ authKind: "key",
148
+ supportLevel: "supported",
149
+ verification: "official",
150
+ documentationUrl: "https://docs.qoder.com/cli/authentication",
151
+ discovery: "live",
152
+ liveModels: true,
153
+ lastVerified: "2026-09-03",
154
+ },
155
+ "qoder-cn": {
156
+ baseUrl: "https://qoder.cn",
157
+ dashboardUrl: "https://qoder.cn/account/integrations",
158
+ adapter: "qoder",
159
+ authKind: "key",
160
+ supportLevel: "supported",
161
+ verification: "official",
162
+ documentationUrl: "https://docs.qoder.cn/en/cli/authentication",
163
+ discovery: "live",
164
+ liveModels: true,
165
+ lastVerified: "2026-09-03",
166
+ },
143
167
  scaleway: openAi("https://api.scaleway.ai/v1", "https://console.scaleway.com/generative-api", { supportLevel: "supported", verification: "official", documentationUrl: "https://www.scaleway.com/en/docs/generative-apis/api-cli/using-generative-apis/", modelsUrl: "https://api.scaleway.ai/v1/models", lastVerified: "2026-08-01" }),
144
168
  sensenova: openAi("https://token.sensenova.cn/v1", "https://console.sensenova.cn", { verification: "official" }),
145
169
  stepfun: openAi("https://api.stepfun.com/v1", "https://platform.stepfun.com", { verification: "official" }),
@@ -160,7 +184,7 @@ const LABELS: Record<string, string> = {
160
184
  ai21: "AI21", baichuan: "Baichuan", deepinfra: "DeepInfra", deepseek: "DeepSeek", doubao: "Doubao",
161
185
  "freemodel-dev": "FreeModel.dev", sambanova: "SambaNova Cloud", nebius: "Nebius Token Factory",
162
186
  novita: "Novita", nscale: "Nscale", nvidia: "NVIDIA NIM",
163
- publicai: "PublicAI", qoder: "Qoder", sensenova: "SenseNova", stepfun: "StepFun", vertex: "Google Vertex AI",
187
+ publicai: "PublicAI", qoder: "Qoder", "qoder-cn": "Qoder CN", sensenova: "SenseNova", stepfun: "StepFun", vertex: "Google Vertex AI",
164
188
  };
165
189
 
166
190
  const referenceNote = "Reference entry only: no safe documented API integration is enabled. Configure it manually only with provider documentation; consumer-web cookies and anti-bot bypasses are intentionally unsupported.";
@@ -0,0 +1,74 @@
1
+ import {
2
+ extractModelEnvelopeRows,
3
+ isValidModelDiscoveryModelId,
4
+ type ProviderModelItemsResult,
5
+ type ProviderModelsApiItem,
6
+ } from "./model-discovery";
7
+
8
+ const GOOGLE_MODEL_PREFIX = "models/";
9
+ const MAX_GENERATION_METHODS = 32;
10
+ const MAX_GENERATION_METHOD_LENGTH = 64;
11
+
12
+ /** Returns the value if it is a positive safe integer; otherwise undefined. */
13
+ function positiveSafeInteger(value: unknown): number | undefined {
14
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0
15
+ ? value
16
+ : undefined;
17
+ }
18
+
19
+ /**
20
+ * Extracts and normalizes supported model items from a Google AI Studio
21
+ * /v1beta/models response payload.
22
+ *
23
+ * Validates the native models[] envelope, strips the 'models/' prefix, filters
24
+ * to rows supporting 'generateContent', maps input/output token limits, and
25
+ * resiliently skips toxic or malformed individual rows.
26
+ */
27
+ export function extractGoogleAiStudioModelItems(
28
+ value: unknown,
29
+ maxModels: number,
30
+ ): ProviderModelItemsResult {
31
+ const envelope = extractModelEnvelopeRows(value, maxModels, ["models"]);
32
+ if (!envelope.ok) return envelope;
33
+
34
+ const items: ProviderModelsApiItem[] = [];
35
+ const seen = new Set<string>();
36
+ for (const raw of envelope.rows) {
37
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
38
+ continue;
39
+ }
40
+ const name = Reflect.get(raw, "name");
41
+ const generationMethods = Reflect.get(raw, "supportedGenerationMethods");
42
+ if (!isValidModelDiscoveryModelId(name)) {
43
+ continue;
44
+ }
45
+ if (generationMethods === undefined) continue;
46
+ if (
47
+ !Array.isArray(generationMethods)
48
+ || generationMethods.length > MAX_GENERATION_METHODS
49
+ || generationMethods.some(method => typeof method !== "string" || method.length > MAX_GENERATION_METHOD_LENGTH)
50
+ ) {
51
+ continue;
52
+ }
53
+ if (!generationMethods.includes("generateContent")) continue;
54
+
55
+ const id = name.startsWith(GOOGLE_MODEL_PREFIX)
56
+ ? name.slice(GOOGLE_MODEL_PREFIX.length)
57
+ : name;
58
+ if (!isValidModelDiscoveryModelId(id) || seen.has(id)) continue;
59
+ seen.add(id);
60
+
61
+ const inputTokenLimit = positiveSafeInteger(Reflect.get(raw, "inputTokenLimit"));
62
+ const outputTokenLimit = positiveSafeInteger(Reflect.get(raw, "outputTokenLimit"));
63
+ items.push({
64
+ id,
65
+ owned_by: "google",
66
+ ...(inputTokenLimit !== undefined
67
+ ? { context_length: inputTokenLimit, max_input_tokens: inputTokenLimit }
68
+ : {}),
69
+ ...(outputTokenLimit !== undefined ? { max_output_tokens: outputTokenLimit } : {}),
70
+ });
71
+ }
72
+ return { ok: true, items, rawCount: envelope.rows.length };
73
+ }
74
+
@@ -10,7 +10,7 @@ import {
10
10
  type CodexAuthPolicyConfig,
11
11
  } from "../codex/auth-context";
12
12
  import { recordCodexUpstreamOutcome, type CodexUpstreamOutcome } from "../codex/routing";
13
- import { extractAccountId } from "../oauth/chatgpt";
13
+ import { inspectChatGptDomainClaim } from "../oauth/chatgpt";
14
14
  import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential, type DataPlaneAdmission } from "../server/auth-cors";
15
15
  import type { CodexAccountMode, OcxConfig, OcxProviderConfig } from "../types";
16
16
  import {
@@ -77,21 +77,45 @@ export function listOpenAiForwardSidecarCandidates(config: OcxConfig): OpenAiFor
77
77
  }];
78
78
  }
79
79
 
80
- function directSidecarHeaders(
81
- incomingHeaders: Headers,
82
- config: CodexAuthPolicyConfig,
83
- admission?: Pick<DataPlaneAdmission, "source">,
84
- ): Headers | undefined {
85
- const bearer = incomingHeaders.get("authorization")?.replace(/^Bearer\s+/i, "").trim();
86
- if (!bearer) return undefined;
87
- const derivedAccountId = extractAccountId(undefined, bearer);
88
- if (!derivedAccountId) return undefined;
80
+ /** An explicit caller bearer/account pair for canonical OpenAI destinations; never persist. */
81
+ export type ExplicitOpenAiCallerAuth = Readonly<{ authorization: string; chatgptAccountId: string }>;
82
+
83
+ function explicitSidecarAuth(incomingHeaders: Headers): ExplicitOpenAiCallerAuth | null {
84
+ // Combined Authorization values must not smuggle a second credential into a snapshot,
85
+ // and only a well-formed ChatGPT-specific account marker is domain evidence — a generic
86
+ // organizations claim is not.
87
+ const bearer = /^Bearer[\t ]+([^\s,]+)$/i.exec(incomingHeaders.get("authorization")?.trim() ?? "")?.[1];
88
+ if (!bearer) return null;
89
+ const claim = inspectChatGptDomainClaim(bearer);
90
+ if (claim.kind !== "valid") return null;
91
+ const derivedAccountId = claim.accountId;
89
92
  const requestedAccountId = incomingHeaders.get("chatgpt-account-id")?.trim();
90
93
  // JWT payloads are decoded locally but not signature-verified. Requiring the caller's
91
94
  // explicit account header, and checking it against the token claim, makes forwarding an
92
95
  // intentional ChatGPT-auth operation instead of silently reclassifying any JWT-shaped
93
96
  // provider credential as a Codex bearer.
94
- if (!requestedAccountId || requestedAccountId !== derivedAccountId) return undefined;
97
+ if (!requestedAccountId || requestedAccountId !== derivedAccountId) return null;
98
+ return { authorization: incomingHeaders.get("authorization")!, chatgptAccountId: requestedAccountId };
99
+ }
100
+
101
+ export function captureExplicitOpenAiCallerAuth(incomingHeaders: Headers, config: OcxConfig): ExplicitOpenAiCallerAuth | null {
102
+ const auth = explicitSidecarAuth(incomingHeaders);
103
+ if (!auth) return null;
104
+ try {
105
+ validateForwardAdmissionCredential(incomingHeaders, config);
106
+ } catch (error) {
107
+ if (error instanceof ForwardAdmissionCredentialError) return null;
108
+ throw error;
109
+ }
110
+ return auth;
111
+ }
112
+
113
+ function directSidecarHeaders(
114
+ incomingHeaders: Headers,
115
+ config: CodexAuthPolicyConfig,
116
+ admission?: Pick<DataPlaneAdmission, "source">,
117
+ ): Headers | undefined {
118
+ if (!explicitSidecarAuth(incomingHeaders)) return undefined;
95
119
  const selected = headersForCodexAuthContext(incomingHeaders, { kind: "main", accountId: null }, config, undefined, admission);
96
120
  return selected;
97
121
  }