@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.
- package/AGENTS_INSTALL.md +9 -1
- package/README.md +11 -5
- package/SPONSORS.md +1 -1
- package/assets/sponsors/orcarouter.png +0 -0
- package/assets/sponsors/packycode.png +0 -0
- package/gui/dist/assets/index-BoBRSehJ.css +1 -0
- package/gui/dist/assets/index-C39tnjXO.js +115 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/packycode.svg +19 -0
- package/gui/dist/provider-icons/qoder.svg +5 -0
- package/package.json +5 -3
- package/src/adapters/anthropic.ts +31 -16
- package/src/adapters/codebuddy/adapter.ts +85 -0
- package/src/adapters/codebuddy/profiles.ts +52 -0
- package/src/adapters/coding-agent/profile.ts +100 -0
- package/src/adapters/coding-agent/protocol.ts +463 -0
- package/src/adapters/coding-agent/turn.ts +353 -0
- package/src/adapters/google.ts +15 -11
- package/src/adapters/mimo-free.ts +3 -0
- package/src/adapters/openai-chat.ts +2 -2
- package/src/adapters/openai-responses.ts +18 -11
- package/src/adapters/qoder/adapter.ts +70 -0
- package/src/adapters/qoder/live-models.ts +89 -0
- package/src/adapters/qoder/profiles.ts +36 -0
- package/src/adapters/registry.ts +12 -0
- package/src/adapters/responses-tool-schema.ts +113 -8
- package/src/claude/inbound.ts +17 -5
- package/src/cli/account-api.ts +18 -3
- package/src/cli/account-auth.ts +8 -1
- package/src/cli/account-extended.ts +2 -1
- package/src/cli/account.ts +1 -0
- package/src/cli/capabilities.ts +15 -1
- package/src/cli/dispatch.ts +2 -0
- package/src/cli/doctor.ts +40 -0
- package/src/cli/effort.ts +24 -8
- package/src/cli/help.ts +2 -0
- package/src/cli/index.ts +29 -2
- package/src/cli/models-runtime.ts +8 -3
- package/src/cli/observe.ts +13 -3
- package/src/cli/provider-runtime.ts +2 -1
- package/src/cli/registry.ts +2 -2
- package/src/cli/system-command.ts +10 -3
- package/src/cli/usage-report.ts +9 -5
- package/src/clients/config-export/zcode.ts +24 -0
- package/src/codex/account-lifecycle.ts +35 -2
- package/src/codex/account-runtime-state.ts +6 -1
- package/src/codex/account-store.ts +72 -9
- package/src/codex/account-usability.ts +3 -2
- package/src/codex/auth-api.ts +113 -26
- package/src/codex/auth-collision.ts +12 -2
- package/src/codex/auth-context.ts +96 -7
- package/src/codex/catalog/parsing.ts +23 -0
- package/src/codex/catalog/provider-fetch.ts +144 -11
- package/src/codex/catalog/sync.ts +14 -0
- package/src/codex/inject.ts +128 -30
- package/src/codex/internal/catalog-writer.ts +3 -0
- package/src/codex/journal.ts +61 -12
- package/src/codex/model-cache.ts +11 -4
- package/src/codex/native-profile-startup.ts +72 -5
- package/src/codex/native-profile-store.ts +2 -2
- package/src/codex/ocx-compaction-history.ts +226 -0
- package/src/codex/project-config-warnings.ts +3 -1
- package/src/codex/quota-auto-refresh.ts +6 -1
- package/src/codex/quota.ts +71 -15
- package/src/codex/reserve-availability.ts +21 -5
- package/src/codex/runtime.ts +45 -1
- package/src/codex/sync.ts +5 -0
- package/src/combos/index.ts +2 -0
- package/src/combos/resolve.ts +52 -0
- package/src/config.ts +59 -0
- package/src/generated/compatibility-version.json +178 -114
- package/src/images/loop.ts +1 -0
- package/src/images/xai-video-client.ts +2 -0
- package/src/integrations/registry.ts +1 -0
- package/src/lib/errors.ts +8 -0
- package/src/lib/privacy.ts +25 -0
- package/src/lib/process-control.ts +52 -8
- package/src/lib/upstream-retry.ts +1 -0
- package/src/oauth/chatgpt.ts +83 -0
- package/src/oauth/health.ts +47 -12
- package/src/oauth/index.ts +46 -8
- package/src/oauth/token-guardian.ts +32 -6
- package/src/oauth/xai.ts +151 -8
- package/src/providers/api-key-selection-capture.ts +10 -0
- package/src/providers/api-key-selection.ts +2 -7
- package/src/providers/caller-authorization.ts +36 -0
- package/src/providers/codebuddy-models.ts +184 -0
- package/src/providers/derive.ts +5 -0
- package/src/providers/free-directory.ts +26 -2
- package/src/providers/google-ai-studio-model-discovery.ts +74 -0
- package/src/providers/openai-sidecar.ts +35 -11
- package/src/providers/opencode-zen-rate-limit.ts +75 -0
- package/src/providers/qoder-models.ts +25 -0
- package/src/providers/quota.ts +15 -0
- package/src/providers/registry.ts +140 -1
- package/src/responses/compaction.ts +4 -0
- package/src/responses/task-input.ts +21 -1
- package/src/router.ts +1 -1
- package/src/server/auth-cors.ts +6 -0
- package/src/server/chat-completions.ts +30 -13
- package/src/server/chat-native.ts +10 -1
- package/src/server/claude-messages.ts +17 -7
- package/src/server/images.ts +3 -2
- package/src/server/index.ts +25 -2
- package/src/server/management/account-selection-stream.ts +13 -4
- package/src/server/management/config-routes.ts +24 -5
- package/src/server/management/logs-usage-routes.ts +5 -1
- package/src/server/management/model-rows.ts +16 -1
- package/src/server/management/native-integration-routes.ts +2 -1
- package/src/server/management/oauth-account-routes.ts +6 -2
- package/src/server/management/provider-routes.ts +33 -2
- package/src/server/management/request-history-routes.ts +4 -2
- package/src/server/management/route-registry.ts +5 -4
- package/src/server/management/shared.ts +66 -3
- package/src/server/management-api.ts +15 -1
- package/src/server/port-reclaim.ts +11 -26
- package/src/server/request-decompress.ts +91 -3
- package/src/server/request-log.ts +16 -0
- package/src/server/responses/codex-ws-wire.ts +1 -1
- package/src/server/responses/collaboration.ts +4 -9
- package/src/server/responses/compact.ts +8 -2
- package/src/server/responses/context-overflow.ts +11 -0
- package/src/server/responses/core.ts +285 -57
- package/src/server/responses/fetch-helpers.ts +18 -7
- package/src/server/responses/policy-fallback.ts +18 -2
- package/src/server/search.ts +2 -2
- package/src/service.ts +128 -9
- package/src/storage/cleanup.ts +77 -45
- package/src/types/accounts.ts +18 -0
- package/src/types/config.ts +43 -1
- package/src/types/provider.ts +56 -0
- package/src/types.ts +4 -0
- package/src/usage/log.ts +24 -0
- package/src/vision/anthropic-describe.ts +1 -0
- package/src/web-search/anthropic-executor.ts +1 -0
- package/src/web-search/loop.ts +1 -0
- package/src/web-search/ollama-executor.ts +127 -0
- package/src/web-search/passthrough-bridge.ts +761 -0
- package/src/web-search/progress-stream.ts +4 -0
- package/gui/dist/assets/index-B5r7LNHN.js +0 -115
- 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
|
-
|
|
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 (
|
|
51
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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=>
|
|
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){
|
|
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
|
|
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
|
+
];
|
package/src/providers/derive.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
if (!
|
|
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
|
|
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
|
}
|