@code-yeongyu/senpi-ai 2026.10.10 → 2026.10.12
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/dist/api/anthropic-messages.js +148 -72
- package/dist/api/anthropic-native-replay-blocks.d.ts +4 -0
- package/dist/api/anthropic-native-replay-blocks.js +28 -0
- package/dist/api/anthropic-tool-references.js +24 -18
- package/dist/api/anthropic-web-search-expiry.d.ts +10 -0
- package/dist/api/anthropic-web-search-expiry.js +104 -0
- package/dist/api/bedrock-converse-stream.js +5 -1
- package/dist/api/google-shared.js +101 -21
- package/dist/api/lazy.js +13 -0
- package/dist/api/mistral-conversations.js +18 -3
- package/dist/api/openai-codex-responses.js +2 -2
- package/dist/api/openai-completions.js +26 -2
- package/dist/api/openai-responses-allowed-tools.d.ts +33 -0
- package/dist/api/openai-responses-allowed-tools.js +108 -0
- package/dist/api/openai-responses-shared.js +19 -0
- package/dist/api/openai-responses.js +48 -59
- package/dist/auth/oauth/anthropic.js +4 -3
- package/dist/auth/oauth/chatgpt-subscription.js +3 -2
- package/dist/auth/oauth/cursor.js +3 -2
- package/dist/auth/oauth/devin-token.js +2 -1
- package/dist/auth/oauth/github-copilot.js +3 -2
- package/dist/auth/oauth/kimi-coding.js +5 -4
- package/dist/auth/oauth/openai-chatgpt.js +2 -1
- package/dist/auth/oauth/openrouter.js +2 -1
- package/dist/auth/oauth/radius.js +5 -5
- package/dist/auth/oauth/xai.js +7 -4
- package/dist/auth/oauth-refresh.js +10 -0
- package/dist/auth/resolve.js +5 -0
- package/dist/model.d.ts +6 -2
- package/dist/openai-responses-compat.d.ts +7 -2
- package/dist/openai-responses-compat.js +15 -2
- package/dist/providers/all.d.ts +1 -1
- package/dist/providers/data/.manifest.json +1 -1
- package/dist/providers/data/amazon-bedrock.json +1 -1
- package/dist/providers/data/anthropic.json +1 -1
- package/dist/providers/data/baseten.json +1 -1
- package/dist/providers/data/chatgpt-subscription.json +1 -1
- package/dist/providers/data/cloudflare-ai-gateway.json +1 -1
- package/dist/providers/data/cloudflare-workers-ai.json +1 -1
- package/dist/providers/data/github-copilot.json +1 -1
- package/dist/providers/data/google-vertex.json +1 -1
- package/dist/providers/data/huggingface.json +1 -1
- package/dist/providers/data/mistral.json +1 -1
- package/dist/providers/data/moonshotai-cn.json +1 -1
- package/dist/providers/data/moonshotai.json +1 -1
- package/dist/providers/data/opencode-go.json +1 -1
- package/dist/providers/data/opencode.json +1 -1
- package/dist/providers/data/opengateway.json +1 -1
- package/dist/providers/data/openrouter.json +1 -1
- package/dist/providers/data/radius.json +1 -1
- package/dist/providers/data/venice.json +1 -1
- package/dist/providers/data/vercel-ai-gateway.json +1 -1
- package/dist/providers/kimi-coding.models.d.ts +1 -1
- package/dist/providers/kimi-coding.models.js +1 -1
- package/dist/providers/opengateway-refresh.js +2 -1
- package/dist/providers/xai.js +7 -1
- package/dist/types.d.ts +25 -2
- package/dist/utils/oauth-refresh-error.d.ts +16 -0
- package/dist/utils/oauth-refresh-error.js +67 -0
- package/dist/utils/prompt-cache-ttl.d.ts +2 -1
- package/dist/utils/prompt-cache-ttl.js +4 -1
- package/dist/utils/provider-failure-description.d.ts +3 -1
- package/dist/utils/provider-failure-description.js +8 -0
- package/dist/utils/retry.d.ts +6 -0
- package/dist/utils/retry.js +10 -0
- package/dist/utils/tool-choice-fallback.d.ts +11 -0
- package/dist/utils/tool-choice-fallback.js +69 -2
- package/package.json +3 -3
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import OpenAI from "openai";
|
|
2
2
|
import { calculateCost, clampThinkingLevel, inferOpenAIThinkingLevelMap, supportsMax, supportsXhigh, } from "../models.js";
|
|
3
|
+
import { supportsAllowedToolChoice } from "../openai-responses-compat.js";
|
|
3
4
|
import { readProviderDiagnostic } from "../provider-diagnostic.js";
|
|
4
5
|
import { formatProviderError, normalizeProviderError } from "../utils/error-body.js";
|
|
5
6
|
import { AssistantMessageEventStream } from "../utils/event-stream.js";
|
|
@@ -10,13 +11,14 @@ import { awaitProviderTransport, openAICompatibleProviderDiagnosticFromError, }
|
|
|
10
11
|
import { getProviderEnvValue } from "../utils/provider-env.js";
|
|
11
12
|
import { retryProviderRequest } from "../utils/provider-retry.js";
|
|
12
13
|
import { sendWithForcedToolChoiceFallback } from "../utils/tool-choice-fallback.js";
|
|
13
|
-
import {
|
|
14
|
+
import { getDeclaredTools, normalizeContext, resolveTranscript } from "../utils/transcript.js";
|
|
14
15
|
import { isCloudflareProvider, resolveCloudflareBaseUrl } from "./cloudflare.js";
|
|
15
16
|
import { createGrammarToolInputProperties } from "./constrained-sampling.js";
|
|
16
17
|
import { withGitHubCopilotFailureNote } from "./github-copilot-errors.js";
|
|
17
18
|
import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.js";
|
|
18
19
|
import { resolveOpenAIClientAuth } from "./openai-client-auth.js";
|
|
19
20
|
import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.js";
|
|
21
|
+
import { applyAllowedToolsChoice, hasRefusedAllowedToolsChoice, isAllowedToolsChoiceRefusal, rememberAllowedToolsChoiceRefusal, restrictToAllowedTools, } from "./openai-responses-allowed-tools.js";
|
|
20
22
|
import { findPromptCacheComparisonResponseId, withPromptCacheComparison, } from "./openai-responses-prompt-cache.js";
|
|
21
23
|
import { convertResponsesMessages, convertResponsesTools, processResponsesStream, resolveResponsesDeferredToolsMode, resolveResponsesToolPlacement, } from "./openai-responses-shared.js";
|
|
22
24
|
import { buildBaseOptions, clampMaxForOpenAI, OPENAI_RESPONSES_RESERVED_BODY_KEYS } from "./simple-options.js";
|
|
@@ -61,6 +63,7 @@ function getCompat(model, env) {
|
|
|
61
63
|
supportsWebSocket: model.compat?.supportsWebSocket ?? isNativeEndpoint,
|
|
62
64
|
supportsRemoteCompactionV2: model.compat?.supportsRemoteCompactionV2 ?? isNativeEndpoint,
|
|
63
65
|
supportsWebSearchPreview: model.compat?.supportsWebSearchPreview ?? isNativeEndpoint,
|
|
66
|
+
webSearchToolType: model.compat?.webSearchToolType ?? "web_search_preview",
|
|
64
67
|
supportsImageGeneration: model.compat?.supportsImageGeneration ?? isNativeEndpoint,
|
|
65
68
|
supportsStrictMode: model.compat?.supportsStrictMode ?? false,
|
|
66
69
|
supportsOpenAIGrammarTools: model.compat?.supportsOpenAIGrammarTools ?? false,
|
|
@@ -70,7 +73,7 @@ function getCompat(model, env) {
|
|
|
70
73
|
supportsConfigurationUpdate: model.compat?.supportsConfigurationUpdate ?? false,
|
|
71
74
|
supportsMaxOutputTokens: model.compat?.supportsMaxOutputTokens ?? true,
|
|
72
75
|
supportsForcedToolChoice: model.compat?.supportsForcedToolChoice ?? true,
|
|
73
|
-
supportsAllowedTools: model
|
|
76
|
+
supportsAllowedTools: supportsAllowedToolChoice(model),
|
|
74
77
|
};
|
|
75
78
|
}
|
|
76
79
|
function isOpenAIResponsesNativeEndpoint(model, env) {
|
|
@@ -100,7 +103,10 @@ function isRecord(value) {
|
|
|
100
103
|
return typeof value === "object" && value !== null;
|
|
101
104
|
}
|
|
102
105
|
function isOpenAiWebSearchPreviewTool(value) {
|
|
103
|
-
return isRecord(value) &&
|
|
106
|
+
return (isRecord(value) &&
|
|
107
|
+
(value.type === "web_search" ||
|
|
108
|
+
value.type === "web_search_preview" ||
|
|
109
|
+
value.type === "web_search_preview_2025_03_11"));
|
|
104
110
|
}
|
|
105
111
|
function sanitizeUnsupportedNativeTools(params, compat) {
|
|
106
112
|
if (compat.supportsWebSearchPreview) {
|
|
@@ -141,53 +147,6 @@ function sanitizeUnsupportedNativeTools(params, compat) {
|
|
|
141
147
|
}
|
|
142
148
|
return sanitized ? sanitized : params;
|
|
143
149
|
}
|
|
144
|
-
function allowedReference(tool) {
|
|
145
|
-
if (tool.type === "function" || tool.type === "custom")
|
|
146
|
-
return { type: tool.type, name: tool.name };
|
|
147
|
-
if (tool.type === "mcp")
|
|
148
|
-
return { type: "mcp", server_label: tool.server_label };
|
|
149
|
-
return { type: tool.type };
|
|
150
|
-
}
|
|
151
|
-
/**
|
|
152
|
-
* senpi#2095: `tools` carries every tool declared this session, so removing a tool never rewrites the
|
|
153
|
-
* cached prefix; the callable subset rides `tool_choice: allowed_tools` instead. Hosted tools a payload
|
|
154
|
-
* hook added stay callable, and deferred tools (declared by transcript items rather than `tools`) are
|
|
155
|
-
* referenced by name. A declared tool a payload hook removed from `tools` is not referenced (senpi#2234).
|
|
156
|
-
* An empty subset forbids tool calls. An explicit `tool_choice` always wins.
|
|
157
|
-
*/
|
|
158
|
-
function applyAllowedToolsChoice(params, context, activeToolNames, compat) {
|
|
159
|
-
if (!compat.supportsAllowedTools || activeToolNames === undefined || params.tool_choice !== undefined) {
|
|
160
|
-
return params;
|
|
161
|
-
}
|
|
162
|
-
const active = new Set(activeToolNames);
|
|
163
|
-
const declaredTools = getCurrentTools(context.messages);
|
|
164
|
-
if (declaredTools.every((tool) => active.has(tool.name)))
|
|
165
|
-
return params;
|
|
166
|
-
const allowed = [];
|
|
167
|
-
const namedInTools = new Set();
|
|
168
|
-
for (const tool of params.tools ?? []) {
|
|
169
|
-
if (tool.type === "function" || tool.type === "custom") {
|
|
170
|
-
namedInTools.add(tool.name);
|
|
171
|
-
if (!active.has(tool.name))
|
|
172
|
-
continue;
|
|
173
|
-
}
|
|
174
|
-
allowed.push(allowedReference(tool));
|
|
175
|
-
}
|
|
176
|
-
// Tools missing from the request-level `tools` placement are the ones transcript items load in place.
|
|
177
|
-
const requestToolNames = new Set(resolveResponsesToolPlacement(context.messages, resolveResponsesDeferredToolsMode(compat) !== undefined).requestTools.map((tool) => tool.name));
|
|
178
|
-
for (const tool of declaredTools) {
|
|
179
|
-
if (namedInTools.has(tool.name) || !active.has(tool.name) || requestToolNames.has(tool.name))
|
|
180
|
-
continue;
|
|
181
|
-
const [converted] = convertResponsesTools([tool], {
|
|
182
|
-
supportsStrictMode: compat.supportsStrictMode,
|
|
183
|
-
supportsOpenAIGrammarTools: compat.supportsOpenAIGrammarTools,
|
|
184
|
-
});
|
|
185
|
-
if (converted)
|
|
186
|
-
allowed.push(allowedReference(converted));
|
|
187
|
-
}
|
|
188
|
-
const toolChoice = allowed.length > 0 ? { type: "allowed_tools", mode: "auto", tools: allowed } : "none";
|
|
189
|
-
return { ...params, tool_choice: toolChoice };
|
|
190
|
-
}
|
|
191
150
|
function formatOpenAIResponsesError(error, provider) {
|
|
192
151
|
const errorMessage = formatProviderError(normalizeProviderError(error), `${provider === "openai" ? "OpenAI" : provider} API error`);
|
|
193
152
|
// Sign in with ChatGPT shares the subscription's usage limit with other apps.
|
|
@@ -234,12 +193,15 @@ export const stream = (model, context, options) => {
|
|
|
234
193
|
}
|
|
235
194
|
params = sanitizeUnsupportedNativeTools(params, compat);
|
|
236
195
|
params = applyAllowedToolsChoice(params, normalizedContext, context.activeToolNames, compat);
|
|
196
|
+
if (hasRefusedAllowedToolsChoice(model))
|
|
197
|
+
params = restrictToAllowedTools(params);
|
|
237
198
|
const limitedTools = limitGitHubCopilotTools(model.provider, params.tools, params.tool_choice);
|
|
238
199
|
if (limitedTools.omittedCount > 0) {
|
|
239
200
|
params = { ...params, tools: limitedTools.tools };
|
|
240
201
|
recordGitHubCopilotToolLimit(output, limitedTools.omittedCount);
|
|
241
202
|
}
|
|
242
203
|
const transport = options?.transport ?? "sse";
|
|
204
|
+
let refusedAllowedToolsOverWebSocket = false;
|
|
243
205
|
if (transport !== "sse" && compat.supportsWebSocket) {
|
|
244
206
|
let websocketStarted = false;
|
|
245
207
|
try {
|
|
@@ -254,9 +216,16 @@ export const stream = (model, context, options) => {
|
|
|
254
216
|
return;
|
|
255
217
|
}
|
|
256
218
|
catch (error) {
|
|
257
|
-
|
|
219
|
+
const refusedAllowedTools = output.content.length === 0 && isAllowedToolsChoiceRefusal(error, params, { beforeContent: true });
|
|
220
|
+
if (!refusedAllowedTools && (transport === "websocket" || websocketStarted)) {
|
|
258
221
|
throw error;
|
|
259
222
|
}
|
|
223
|
+
if (refusedAllowedTools) {
|
|
224
|
+
params = restrictToAllowedTools(params);
|
|
225
|
+
refusedAllowedToolsOverWebSocket = true;
|
|
226
|
+
output.stopReason = "pending";
|
|
227
|
+
delete output.errorMessage;
|
|
228
|
+
}
|
|
260
229
|
}
|
|
261
230
|
}
|
|
262
231
|
const requestOptions = {
|
|
@@ -264,26 +233,46 @@ export const stream = (model, context, options) => {
|
|
|
264
233
|
...(options?.timeoutMs !== undefined ? { timeout: options.timeoutMs } : {}),
|
|
265
234
|
maxRetries: 0,
|
|
266
235
|
};
|
|
267
|
-
const
|
|
236
|
+
const sendOnce = async (body) => {
|
|
268
237
|
const sent = await sendWithForcedToolChoiceFallback({
|
|
269
238
|
target: model,
|
|
270
|
-
params,
|
|
239
|
+
params: body,
|
|
271
240
|
acceptsForcedToolChoice: compat.supportsForcedToolChoice,
|
|
272
241
|
isForced: isForcedOpenAIResponsesToolChoice,
|
|
273
|
-
send: (
|
|
274
|
-
? awaitProviderTransport(() => client.responses.create(
|
|
275
|
-
: client.responses.create(
|
|
242
|
+
send: (request) => model.provider === "github-copilot"
|
|
243
|
+
? awaitProviderTransport(() => client.responses.create(request, requestOptions).withResponse(), openAICompatibleProviderDiagnosticFromError)
|
|
244
|
+
: client.responses.create(request, requestOptions).withResponse(),
|
|
276
245
|
});
|
|
277
246
|
params = sent.params;
|
|
278
247
|
return sent.result;
|
|
279
248
|
};
|
|
249
|
+
// senpi#3080: an endpoint that refuses `allowed_tools` gets the request restricted to the active
|
|
250
|
+
// tools; the model is remembered once that restricted request is accepted.
|
|
251
|
+
const createRequest = async () => {
|
|
252
|
+
try {
|
|
253
|
+
const result = await sendOnce(params);
|
|
254
|
+
if (refusedAllowedToolsOverWebSocket)
|
|
255
|
+
rememberAllowedToolsChoiceRefusal(model);
|
|
256
|
+
return result;
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
if (!isAllowedToolsChoiceRefusal(error, params))
|
|
260
|
+
throw error;
|
|
261
|
+
params = restrictToAllowedTools(params);
|
|
262
|
+
const result = await sendOnce(params);
|
|
263
|
+
rememberAllowedToolsChoiceRefusal(model);
|
|
264
|
+
return result;
|
|
265
|
+
}
|
|
266
|
+
};
|
|
280
267
|
const { data: openaiStream, response } = await retryProviderRequest(() => createRequest(), {
|
|
281
268
|
maxRetries: options?.maxRetries,
|
|
282
269
|
maxRetryDelayMs: options?.maxRetryDelayMs,
|
|
283
270
|
signal: options?.signal,
|
|
284
271
|
});
|
|
285
272
|
await options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model);
|
|
286
|
-
|
|
273
|
+
// A WebSocket attempt that fell through after its refusal already pushed `start`.
|
|
274
|
+
if (!refusedAllowedToolsOverWebSocket)
|
|
275
|
+
stream.push({ type: "start", partial: output });
|
|
287
276
|
await processResponsesStream(openaiStream, output, stream, model, {
|
|
288
277
|
onProviderStreamEvent: options?.onProviderStreamEvent,
|
|
289
278
|
serviceTier: options?.serviceTier,
|
|
@@ -861,9 +850,9 @@ function buildWebSocketHeaders(model, context, apiKey, optionsHeaders, sessionId
|
|
|
861
850
|
function getServiceTierCostMultiplier(model, serviceTier) {
|
|
862
851
|
switch (serviceTier) {
|
|
863
852
|
case "ultrafast":
|
|
864
|
-
// OpenAI
|
|
853
|
+
// OpenAI prices GPT-6 Astra and GPT-6.1 Sol Ultrafast at 6x Standard on every
|
|
865
854
|
// token class and context tier. Any other model keeps its base rate.
|
|
866
|
-
return
|
|
855
|
+
return ["gpt-6-astra", "gpt-6.1-sol"].includes(model.upstreamModelId ?? model.id) ? 6 : 1;
|
|
867
856
|
case "flex":
|
|
868
857
|
return 0.5;
|
|
869
858
|
case "priority":
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* NOTE: This module uses Node.js http.createServer for the OAuth callback server.
|
|
5
5
|
* It is only intended for CLI use, not browser environments.
|
|
6
6
|
*/
|
|
7
|
+
import { OAuthTokenEndpointError } from "../../utils/oauth-refresh-error.js";
|
|
7
8
|
import { getProviderEnvValue } from "../../utils/provider-env.js";
|
|
8
9
|
import { PREFERRED_CALLBACK_PORT, startCallbackListener, } from "./anthropic-callback-listener.js";
|
|
9
10
|
import { parseAuthorizationInput } from "./authorization-input.js";
|
|
@@ -57,7 +58,7 @@ async function postJson(url, body, signal) {
|
|
|
57
58
|
});
|
|
58
59
|
const responseBody = await response.text();
|
|
59
60
|
if (!response.ok) {
|
|
60
|
-
throw new
|
|
61
|
+
throw new OAuthTokenEndpointError(`HTTP request failed. status=${response.status}; url=${url}; body=${responseBody}`, response.status);
|
|
61
62
|
}
|
|
62
63
|
return responseBody;
|
|
63
64
|
}
|
|
@@ -74,7 +75,7 @@ async function exchangeAuthorizationCode(code, state, verifier, redirectUri, sig
|
|
|
74
75
|
}, signal);
|
|
75
76
|
}
|
|
76
77
|
catch (error) {
|
|
77
|
-
throw new Error(`Token exchange request failed. url=${TOKEN_URL}; redirect_uri=${redirectUri}; response_type=authorization_code; details=${formatErrorDetails(error)}
|
|
78
|
+
throw new Error(`Token exchange request failed. url=${TOKEN_URL}; redirect_uri=${redirectUri}; response_type=authorization_code; details=${formatErrorDetails(error)}`, { cause: error });
|
|
78
79
|
}
|
|
79
80
|
let tokenData;
|
|
80
81
|
try {
|
|
@@ -240,7 +241,7 @@ async function refreshAnthropicToken(refreshToken, signal) {
|
|
|
240
241
|
}, signal);
|
|
241
242
|
}
|
|
242
243
|
catch (error) {
|
|
243
|
-
throw new Error(`Anthropic token refresh request failed. url=${TOKEN_URL}; details=${formatErrorDetails(error)}
|
|
244
|
+
throw new Error(`Anthropic token refresh request failed. url=${TOKEN_URL}; details=${formatErrorDetails(error)}`, { cause: error });
|
|
244
245
|
}
|
|
245
246
|
let data;
|
|
246
247
|
try {
|
|
@@ -12,6 +12,7 @@ if (typeof process !== "undefined" && (process.versions?.node || process.version
|
|
|
12
12
|
});
|
|
13
13
|
}
|
|
14
14
|
import { extractChatGptSubscriptionAccountId } from "../../utils/chatgpt-subscription-auth.js";
|
|
15
|
+
import { OAuthTokenEndpointError } from "../../utils/oauth-refresh-error.js";
|
|
15
16
|
import { getProviderEnvValue } from "../../utils/provider-env.js";
|
|
16
17
|
import { getWireIdentity } from "../../wire-identity.js";
|
|
17
18
|
import { startOAuthCallbackServer, waitForCallbackOrManualInput } from "./callback-server.js";
|
|
@@ -80,7 +81,7 @@ async function fetchWithLoginCancellation(input, init) {
|
|
|
80
81
|
async function readTokenResponse(response, operation) {
|
|
81
82
|
if (!response.ok) {
|
|
82
83
|
const text = await response.text().catch(() => "");
|
|
83
|
-
throw new
|
|
84
|
+
throw new OAuthTokenEndpointError(`ChatGPT Subscription token ${operation} failed (${response.status}): ${text || response.statusText}`, response.status);
|
|
84
85
|
}
|
|
85
86
|
const rawJson = await response.json();
|
|
86
87
|
const json = rawJson;
|
|
@@ -123,7 +124,7 @@ async function refreshAccessToken(refreshToken, signal) {
|
|
|
123
124
|
});
|
|
124
125
|
}
|
|
125
126
|
catch (error) {
|
|
126
|
-
throw new Error(`ChatGPT Subscription token refresh error: ${error instanceof Error ? error.message : String(error)}
|
|
127
|
+
throw new Error(`ChatGPT Subscription token refresh error: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
127
128
|
}
|
|
128
129
|
return readTokenResponse(response, "refresh");
|
|
129
130
|
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* (or a dashboard user API key) at `auth/exchange_user_api_key` for a fresh
|
|
11
11
|
* session JWT.
|
|
12
12
|
*/
|
|
13
|
+
import { OAuthTokenEndpointError } from "../../utils/oauth-refresh-error.js";
|
|
13
14
|
import { generatePKCE } from "./pkce.js";
|
|
14
15
|
const CURSOR_LOGIN_URL = "https://cursor.com/loginDeepControl";
|
|
15
16
|
const CURSOR_POLL_URL = "https://api2.cursor.sh/auth/poll";
|
|
@@ -205,12 +206,12 @@ async function refreshCursorCredential(credential, signal) {
|
|
|
205
206
|
}
|
|
206
207
|
catch (error) {
|
|
207
208
|
if (signal.aborted)
|
|
208
|
-
throw new Error("Cursor token refresh cancelled");
|
|
209
|
+
throw new Error("Cursor token refresh cancelled", { cause: signal.reason });
|
|
209
210
|
throw error;
|
|
210
211
|
}
|
|
211
212
|
const body = await readJsonBody(response);
|
|
212
213
|
if (!response.ok) {
|
|
213
|
-
throw new
|
|
214
|
+
throw new OAuthTokenEndpointError(`Cursor token refresh failed (${describeFailure(response.status, body)})`, response.status);
|
|
214
215
|
}
|
|
215
216
|
const access = requiredString(body ?? {}, "accessToken");
|
|
216
217
|
const rotated = body?.refreshToken;
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* "token" that serves as both the access and the refresh credential. Expiry is
|
|
8
8
|
* read from the JWT itself because the response carries no expires_in.
|
|
9
9
|
*/
|
|
10
|
+
import { OAuthTokenEndpointError } from "../../utils/oauth-refresh-error.js";
|
|
10
11
|
export const DEVIN_TOKEN_URL = "https://api.devin.ai/auth/cli/token";
|
|
11
12
|
export const DEVIN_API_ENDPOINT = "https://api.devin.ai";
|
|
12
13
|
/** Devin issues long-lived CLI tokens; used when the JWT carries no usable exp. */
|
|
@@ -91,7 +92,7 @@ export async function exchangeDevinAuthorizationCode(code, verifier, signal) {
|
|
|
91
92
|
}
|
|
92
93
|
if (!response.ok) {
|
|
93
94
|
const detail = errorDetail(body);
|
|
94
|
-
throw new
|
|
95
|
+
throw new OAuthTokenEndpointError(`Devin OAuth token exchange failed (HTTP ${response.status})${detail ? `: ${detail}` : ""}`, response.status);
|
|
95
96
|
}
|
|
96
97
|
if (typeof body.token !== "string" || body.token.length === 0) {
|
|
97
98
|
throw new Error('Devin OAuth response carries no "token"');
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { GITHUB_COPILOT_INDIVIDUAL_BASE_URL, githubCopilotTokenField, parseGitHubCopilotApiEndpoint, resolveGitHubCopilotBaseUrl, } from "../../api/github-copilot-endpoint.js";
|
|
5
5
|
import { GITHUB_COPILOT_REJECTED_TOKEN_STATUSES } from "../../api/github-copilot-headers.js";
|
|
6
|
+
import { OAuthTokenEndpointError } from "../../utils/oauth-refresh-error.js";
|
|
6
7
|
import { sleep } from "../../utils/sleep.js";
|
|
7
8
|
import { pollOAuthDeviceCodeFlow } from "./device-code.js";
|
|
8
9
|
import { parseGitHubCopilotModelCatalog } from "./github-copilot-model-catalog.js";
|
|
@@ -79,7 +80,7 @@ async function fetchGitHubCopilotModels(copilotToken, enterpriseDomain, signal,
|
|
|
79
80
|
},
|
|
80
81
|
}, signal, retryPolicy);
|
|
81
82
|
if (!response.ok) {
|
|
82
|
-
throw new
|
|
83
|
+
throw new OAuthTokenEndpointError(`${response.status} ${response.statusText}: ${await response.text()}`, response.status);
|
|
83
84
|
}
|
|
84
85
|
return parseGitHubCopilotModelCatalog(await response.json(), allowPolicyFallback);
|
|
85
86
|
}
|
|
@@ -87,7 +88,7 @@ async function fetchJson(url, init) {
|
|
|
87
88
|
const response = await fetch(url, init);
|
|
88
89
|
if (!response.ok) {
|
|
89
90
|
const text = await response.text();
|
|
90
|
-
throw new
|
|
91
|
+
throw new OAuthTokenEndpointError(`${response.status} ${response.statusText}: ${text}`, response.status);
|
|
91
92
|
}
|
|
92
93
|
return response.json();
|
|
93
94
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* access token authenticates requests to that region's coding API as an
|
|
7
7
|
* `Authorization: Bearer` header; see kimi-region.ts for the host table.
|
|
8
8
|
*/
|
|
9
|
+
import { OAuthTokenEndpointError } from "../../utils/oauth-refresh-error.js";
|
|
9
10
|
import { sleep } from "../../utils/sleep.js";
|
|
10
11
|
import { pollOAuthDeviceCodeFlow } from "./device-code.js";
|
|
11
12
|
import { kimiCodeIdentityHeaders } from "./kimi-identity.js";
|
|
@@ -178,7 +179,7 @@ async function refreshToken(oauthHost, refreshTokenValue, signal) {
|
|
|
178
179
|
await sleep(1000 * 2 ** (attempt - 1), signal);
|
|
179
180
|
}
|
|
180
181
|
if (signal.aborted) {
|
|
181
|
-
throw new Error("Kimi Code token refresh aborted");
|
|
182
|
+
throw new Error("Kimi Code token refresh aborted", { cause: signal.reason });
|
|
182
183
|
}
|
|
183
184
|
let response;
|
|
184
185
|
try {
|
|
@@ -208,14 +209,14 @@ async function refreshToken(oauthHost, refreshTokenValue, signal) {
|
|
|
208
209
|
// Unauthorized: the stored credential is dead; Models clears it and prompts re-login.
|
|
209
210
|
if (response.status === 401 || response.status === 403 || json?.error === "invalid_grant") {
|
|
210
211
|
const description = typeof json?.error_description === "string" ? `: ${json.error_description}` : "";
|
|
211
|
-
throw new
|
|
212
|
+
throw new OAuthTokenEndpointError(`Kimi Code token refresh unauthorized (status ${response.status})${description}`, response.status);
|
|
212
213
|
}
|
|
213
214
|
if (isRetryableRefreshFailure(response) && attempt < REFRESH_MAX_RETRIES) {
|
|
214
|
-
lastError = new
|
|
215
|
+
lastError = new OAuthTokenEndpointError(`Kimi Code token refresh failed with status ${response.status}`, response.status);
|
|
215
216
|
continue;
|
|
216
217
|
}
|
|
217
218
|
const text = JSON.stringify(json);
|
|
218
|
-
throw new
|
|
219
|
+
throw new OAuthTokenEndpointError(`Kimi Code token refresh failed with status ${response.status}${text ? `: ${text}` : ""}`, response.status);
|
|
219
220
|
}
|
|
220
221
|
throw lastError ?? new Error("Kimi Code token refresh failed");
|
|
221
222
|
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { randomBytes } from "node:crypto";
|
|
8
8
|
import { createServer } from "node:http";
|
|
9
9
|
import { oauthErrorHtml, oauthSuccessHtml } from "../../utils/oauth-page.js";
|
|
10
|
+
import { OAuthTokenEndpointError } from "../../utils/oauth-refresh-error.js";
|
|
10
11
|
import { getProviderEnvValue } from "../../utils/provider-env.js";
|
|
11
12
|
import { generatePKCE } from "./pkce.js";
|
|
12
13
|
// every login registers a new client with this ID; OpenAI returns the issued client ID in the callback
|
|
@@ -119,7 +120,7 @@ async function requestToken(body, signal) {
|
|
|
119
120
|
});
|
|
120
121
|
if (!response.ok) {
|
|
121
122
|
const responseBody = await response.text().catch(() => "");
|
|
122
|
-
throw new
|
|
123
|
+
throw new OAuthTokenEndpointError(`OpenAI OAuth token request failed (${response.status}): ${responseBody || response.statusText}`, response.status);
|
|
123
124
|
}
|
|
124
125
|
const data = await response.json();
|
|
125
126
|
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* NOTE: This module uses node:http (via callback-server.ts) for the OAuth callback server.
|
|
11
11
|
* It is only intended for CLI use, not browser environments.
|
|
12
12
|
*/
|
|
13
|
+
import { OAuthTokenEndpointError } from "../../utils/oauth-refresh-error.js";
|
|
13
14
|
import { getProviderEnvValue } from "../../utils/provider-env.js";
|
|
14
15
|
import { startOAuthCallbackServer, waitForCallbackOrManualInput } from "./callback-server.js";
|
|
15
16
|
import { generatePKCE } from "./pkce.js";
|
|
@@ -88,7 +89,7 @@ async function exchangeAuthorizationCode(code, verifier, signal) {
|
|
|
88
89
|
}
|
|
89
90
|
if (!response.ok) {
|
|
90
91
|
const detail = errorDetail(body);
|
|
91
|
-
throw new
|
|
92
|
+
throw new OAuthTokenEndpointError(`OpenRouter OAuth key exchange failed (HTTP ${response.status})${detail ? `: ${detail}` : ""}`, response.status);
|
|
92
93
|
}
|
|
93
94
|
if (typeof body.key !== "string" || body.key.length === 0) {
|
|
94
95
|
throw new Error('OpenRouter OAuth response carries no "key"');
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* It is only intended for CLI use, not browser environments.
|
|
10
10
|
*/
|
|
11
11
|
import { normalizeRadiusGatewayUrl } from "../../providers/radius-config.js";
|
|
12
|
+
import { OAuthTokenEndpointError } from "../../utils/oauth-refresh-error.js";
|
|
12
13
|
import { startOAuthCallbackServer } from "./callback-server.js";
|
|
13
14
|
import { pollOAuthDeviceCodeFlow } from "./device-code.js";
|
|
14
15
|
import { generatePKCE } from "./pkce.js";
|
|
@@ -36,15 +37,14 @@ async function loadRadiusOAuthDiscovery(gateway, signal) {
|
|
|
36
37
|
}
|
|
37
38
|
return { authorizationEndpoint: discovery.authorizationEndpoint };
|
|
38
39
|
}
|
|
39
|
-
class OAuthResponseError extends
|
|
40
|
+
class OAuthResponseError extends OAuthTokenEndpointError {
|
|
40
41
|
constructor(status, oauthError, description, message) {
|
|
41
42
|
const detail = oauthError
|
|
42
43
|
? description
|
|
43
44
|
? `${oauthError}: ${description}`
|
|
44
45
|
: oauthError
|
|
45
46
|
: description || String(status);
|
|
46
|
-
super(`${message}: ${detail}
|
|
47
|
-
this.status = status;
|
|
47
|
+
super(`${message}: ${detail}`, status);
|
|
48
48
|
this.oauthError = oauthError;
|
|
49
49
|
}
|
|
50
50
|
}
|
|
@@ -76,7 +76,7 @@ async function requestOAuthToken(gateway, body, signal) {
|
|
|
76
76
|
}
|
|
77
77
|
catch (error) {
|
|
78
78
|
if (signal.aborted) {
|
|
79
|
-
throw new Error("Login cancelled");
|
|
79
|
+
throw new Error("Login cancelled", { cause: signal.reason });
|
|
80
80
|
}
|
|
81
81
|
throw error;
|
|
82
82
|
}
|
|
@@ -149,7 +149,7 @@ async function requestDeviceAuthorization(gateway, signal) {
|
|
|
149
149
|
}
|
|
150
150
|
catch (error) {
|
|
151
151
|
if (signal.aborted) {
|
|
152
|
-
throw new Error("Login cancelled");
|
|
152
|
+
throw new Error("Login cancelled", { cause: signal.reason });
|
|
153
153
|
}
|
|
154
154
|
throw error;
|
|
155
155
|
}
|
package/dist/auth/oauth/xai.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* xAI OAuth device-code flow.
|
|
3
3
|
*/
|
|
4
|
+
import { OAuthTokenEndpointError } from "../../utils/oauth-refresh-error.js";
|
|
4
5
|
import { pollOAuthDeviceCodeFlow } from "./device-code.js";
|
|
5
6
|
const XAI_CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828";
|
|
6
7
|
const XAI_SCOPE = "openid profile email offline_access grok-cli:access api:access";
|
|
@@ -53,7 +54,7 @@ async function postForm(url, fields, signal) {
|
|
|
53
54
|
}
|
|
54
55
|
catch (error) {
|
|
55
56
|
if (signal.aborted) {
|
|
56
|
-
throw new Error("Login cancelled");
|
|
57
|
+
throw new Error("Login cancelled", { cause: signal.reason });
|
|
57
58
|
}
|
|
58
59
|
throw error;
|
|
59
60
|
}
|
|
@@ -64,9 +65,11 @@ async function postForm(url, fields, signal) {
|
|
|
64
65
|
}
|
|
65
66
|
catch {
|
|
66
67
|
if (signal.aborted) {
|
|
67
|
-
throw new Error("Login cancelled");
|
|
68
|
+
throw new Error("Login cancelled", { cause: signal.reason });
|
|
68
69
|
}
|
|
69
|
-
|
|
70
|
+
const message = `xAI OAuth returned invalid JSON (HTTP ${response.status})`;
|
|
71
|
+
// An error page (an HTML 503 from a proxy, say) still carries the status the retry decision needs.
|
|
72
|
+
throw response.ok ? new Error(message) : new OAuthTokenEndpointError(message, response.status);
|
|
70
73
|
}
|
|
71
74
|
return {
|
|
72
75
|
ok: response.ok,
|
|
@@ -78,7 +81,7 @@ function requestFailure(action, response) {
|
|
|
78
81
|
const error = typeof response.body.error === "string" ? response.body.error : undefined;
|
|
79
82
|
const description = typeof response.body.error_description === "string" ? response.body.error_description : undefined;
|
|
80
83
|
const detail = [error, description].filter(Boolean).join(": ");
|
|
81
|
-
return new
|
|
84
|
+
return new OAuthTokenEndpointError(`xAI OAuth ${action} failed (HTTP ${response.status})${detail ? `: ${detail}` : ""}`, response.status);
|
|
82
85
|
}
|
|
83
86
|
function parseDeviceCode(body) {
|
|
84
87
|
// RFC 8628 allows interval 0 (no minimum wait); fall back to the poller's
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* superseded catalog refresh stops waiting but never aborts the exchange.
|
|
16
16
|
*/
|
|
17
17
|
import { raceWithAbortSignal } from "../utils/abort.js";
|
|
18
|
+
import { oauthRefreshFailureCause } from "../utils/oauth-refresh-error.js";
|
|
18
19
|
import { mergeRefreshed, mergeRefreshedSlot, projectSlot } from "./pool/slots.js";
|
|
19
20
|
export const DEFAULT_OAUTH_REFRESH_TIMEOUT_MS = 15_000;
|
|
20
21
|
/** The provider rejected or timed out the token exchange. */
|
|
@@ -103,6 +104,15 @@ async function exchangeAndStore(request, signal) {
|
|
|
103
104
|
signal.throwIfAborted();
|
|
104
105
|
}
|
|
105
106
|
catch (error) {
|
|
107
|
+
signal.throwIfAborted();
|
|
108
|
+
const causeClass = oauthRefreshFailureCause(error);
|
|
109
|
+
if (causeClass !== undefined) {
|
|
110
|
+
console.warn("OAuth refresh unavailable", JSON.stringify({
|
|
111
|
+
provider: providerId,
|
|
112
|
+
...(slotName ? { slot: slotName } : {}),
|
|
113
|
+
cause: causeClass,
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
106
116
|
throw new OAuthRefreshExchangeError(error);
|
|
107
117
|
}
|
|
108
118
|
try {
|
package/dist/auth/resolve.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { operationSignal, raceWithAbortSignal } from "../utils/abort.js";
|
|
2
2
|
import { ModelsError } from "../utils/models-error.js";
|
|
3
|
+
import { classifyOAuthRefreshFailure, OAuthRefreshUnavailableError } from "../utils/oauth-refresh-error.js";
|
|
3
4
|
import { OAuthRefreshExchangeError, OAuthRefreshStoreError, projectOAuthSlot, refreshOAuthCredential, } from "./oauth-refresh.js";
|
|
4
5
|
import { projectSlot } from "./pool/slots.js";
|
|
5
6
|
export { ModelsError } from "../utils/models-error.js";
|
|
@@ -82,6 +83,9 @@ export function oauthRefreshModelsError(error, providerId) {
|
|
|
82
83
|
if (error instanceof ModelsError)
|
|
83
84
|
return error;
|
|
84
85
|
if (error instanceof OAuthRefreshExchangeError) {
|
|
86
|
+
if (classifyOAuthRefreshFailure(error.cause) === "transient") {
|
|
87
|
+
return new OAuthRefreshUnavailableError(providerId, error.cause);
|
|
88
|
+
}
|
|
85
89
|
return new ModelsError("oauth", `OAuth refresh failed for ${providerId}`, { cause: error.cause });
|
|
86
90
|
}
|
|
87
91
|
const cause = error instanceof OAuthRefreshStoreError ? error.cause : error;
|
|
@@ -112,6 +116,7 @@ async function resolveStoredOAuth(credentials, providerId, oauth, stored, authCo
|
|
|
112
116
|
});
|
|
113
117
|
}
|
|
114
118
|
catch (error) {
|
|
119
|
+
signal.throwIfAborted();
|
|
115
120
|
throw oauthRefreshModelsError(error, providerId);
|
|
116
121
|
}
|
|
117
122
|
if (post?.type !== "oauth")
|
package/dist/model.d.ts
CHANGED
|
@@ -44,8 +44,12 @@ export interface Model<TApi extends Api> extends BaseModel<TApi> {
|
|
|
44
44
|
* probe; `modelSupportsAssistantPrefill` also applies the request settings.
|
|
45
45
|
*/
|
|
46
46
|
supportsAssistantPrefill?: boolean;
|
|
47
|
-
/**
|
|
48
|
-
compat?: TApi extends "openai-completions" ? OpenAICompletionsCompat : TApi extends "openai-responses" | "openai-codex-responses" | "azure-openai-responses" ? OpenAIResponsesCompat : TApi extends "anthropic-messages" ? AnthropicMessagesCompat : TApi extends "bedrock-converse-stream" ? BedrockCompat : TApi extends "mistral-conversations" ? MistralConversationsCompat : TApi extends "cursor-agent" ? CursorAgentCompat : TApi extends "devin-agent" ? DevinAgentCompat : never;
|
|
47
|
+
/** API compatibility overrides. Hosted search on unverified routes requires explicit opt-in. */
|
|
48
|
+
compat?: TApi extends "openai-completions" ? OpenAICompletionsCompat : TApi extends "openai-responses" | "openai-codex-responses" | "azure-openai-responses" ? OpenAIResponsesCompat : TApi extends "anthropic-messages" ? AnthropicMessagesCompat : TApi extends "bedrock-converse-stream" ? BedrockCompat : TApi extends "mistral-conversations" ? MistralConversationsCompat : TApi extends "cursor-agent" ? CursorAgentCompat : TApi extends "devin-agent" ? DevinAgentCompat : TApi extends "google-generative-ai" | "google-vertex" ? GoogleCompat : never;
|
|
49
|
+
}
|
|
50
|
+
export interface GoogleCompat {
|
|
51
|
+
/** Opt in to Google Search grounding, including mixed function-tool requests. */
|
|
52
|
+
supportsWebSearch?: boolean;
|
|
49
53
|
}
|
|
50
54
|
/** Devin (Cascade) model metadata the transport branches on. */
|
|
51
55
|
export interface DevinAgentCompat {
|
|
@@ -42,10 +42,15 @@ export interface OpenAIResponsesCompat {
|
|
|
42
42
|
* Whether the model accepts `tool_choice: { type: "allowed_tools" }`. When set, callers keep every
|
|
43
43
|
* declared tool in `tools` and restrict the callable subset through `Context.activeToolNames`, so a
|
|
44
44
|
* shrinking tool set does not rewrite the cached prompt prefix. Honored by the `openai-responses`
|
|
45
|
-
* adapter.
|
|
45
|
+
* adapter, and only on the native OpenAI API hosts (`api.openai.com` and its regional `*.api.openai.com`
|
|
46
|
+
* hosts): Responses-compatible gateways reject the choice (senpi#3080), and since a provider `baseUrl`
|
|
47
|
+
* override keeps the catalog compat, the flag cannot opt a gateway in. Default: false.
|
|
46
48
|
*/
|
|
47
49
|
supportsAllowedTools?: boolean;
|
|
48
50
|
}
|
|
49
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* Whether requests to `model` restrict tools through `allowed_tools`: its compat declares support and it is
|
|
53
|
+
* served by the native OpenAI endpoint (see {@link OpenAIResponsesCompat.supportsAllowedTools}).
|
|
54
|
+
*/
|
|
50
55
|
export declare function supportsAllowedToolChoice(model: Model<Api>): boolean;
|
|
51
56
|
//# sourceMappingURL=openai-responses-compat.d.ts.map
|
|
@@ -1,5 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
function isNativeOpenAIEndpoint(baseUrl) {
|
|
2
|
+
try {
|
|
3
|
+
const { hostname } = new URL(baseUrl || "https://api.openai.com/v1");
|
|
4
|
+
return hostname === "api.openai.com" || hostname.endsWith(".api.openai.com");
|
|
5
|
+
}
|
|
6
|
+
catch {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Whether requests to `model` restrict tools through `allowed_tools`: its compat declares support and it is
|
|
12
|
+
* served by the native OpenAI endpoint (see {@link OpenAIResponsesCompat.supportsAllowedTools}).
|
|
13
|
+
*/
|
|
2
14
|
export function supportsAllowedToolChoice(model) {
|
|
3
|
-
return model.compat?.supportsAllowedTools === true
|
|
15
|
+
return (model.compat?.supportsAllowedTools === true &&
|
|
16
|
+
isNativeOpenAIEndpoint(model.baseUrl));
|
|
4
17
|
}
|
|
5
18
|
//# sourceMappingURL=openai-responses-compat.js.map
|
package/dist/providers/all.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"schemaVersion":6,"generatedAt":"2026-10-
|
|
1
|
+
{"schemaVersion":6,"generatedAt":"2026-10-12T15:26:30.855Z","structureHash":"f696be8fbb6471faefafe7b9ae99f4c0db4391398bf4108f56024ed2712ebf88","files":{"alibaba-token-plan.json":"16b3c6c78d72c73ca34a1e79ce71c0cf36ba409d02e83a17396ce0949a1d5363","amazon-bedrock.json":"4a95e6bf974e5ca967d5c050df580262e13573e42b3186edb448fa11a302f09f","ant-ling.json":"628feab3ef6c7d80ce96f75999804e1d9f05fec3851e5302e30fff3b69db90f6","anthropic.json":"111384e9f5845e0247bd34edf2400ef7720fcbdac140b944cf8878192d8bd6ab","azure-openai-responses.json":"a3e52bd78f0220aa32bce519e13955b8e6e567860416343844cbe19902f4a682","bai.json":"7e6861aaf27d8af34e1771c6092602194fcec0a0046e6adfab9c3ee54d32b99f","baseten.json":"1fcdab8daf2881bd099eb08f9416f7df5bf39975937d2a63b841542bd032f2e6","cerebras.json":"3eccb58c5de3d0dc7ba49dc6e477ba29f00cb3150ce0a35cc90d59e6b5ab368e","chatgpt-subscription.json":"42b88c93d717b2a7469b71da7da5adf87347d0d9cc0f264b39117f0f940b51c0","cloudflare-ai-gateway.json":"5c8782fdbf2477f620b238da4344b0bcc0948b8c177461e63e7e5ba256f769c7","cloudflare-workers-ai.json":"f351e80493dea7b7a2c31dceb086ee804a88d67c5f8a3c823ea907991fe5d659","deepseek.json":"10a296fb3e898f7715c80890c8af0af4f5fd58f22dbcd6612fdc5d762157ccdc","fireworks.json":"c3dc21dc2e95428fc9a38e3840c05bbf90f94e4c25dee0eedf0b385b49ff65a4","github-copilot.json":"2c5a84983477a7c4f6c001088a1e043ee959ce58c99fea6eb843e0286228664b","google-vertex.json":"96ecc19bf20a706f99ae658b021a0ee5438146119e1e99de5fd824a01fcfd81b","google.json":"c0f5a633e5e5a5044432db3e537f4bc674d4de18df09325c6349547fcc13cabe","groq.json":"910952ad9dad07856983459d63fce0148dba1203edd7b10d801a77c5824f3405","huggingface.json":"6399e958790a9d90c21fb55d1cbef8569427a1ceb28de86dadeb3b4454ecfade","meta.json":"0c7e9a370a7005c5a89dc94f0f93bc67b8935c3f802de324f11af6ff56fff429","minimax-cn.json":"f962c20ee6a6a342820f778e9dc363a18a524bf11998f6e2c57f2989915d62e5","minimax.json":"17a432f80d0ea6dd26eade80a4314250d3a855678601728a5f70dd8fa0d21dd2","mistral.json":"27aa66c71a0504b53dc87ec194286fa7e70b99747fc8df9b709b23b3548afe50","moonshotai-cn.json":"18954b55dc5edecc174d8a07a2c5993416dd9d3ce52e411438035c16ee982e8e","moonshotai.json":"62d08fd5beead025a56d5f2e4227d8516b9c5034275b1b36d761a8625918f2b9","nvidia.json":"a75e963dc81779e3eec01c051b12277c15ec37c5b94ab51ece9e9ccb3b16407c","openai.json":"9104538118053eed57f90852332b6ad274a4e4792389bdbc9cfca1ae400c705a","opencode-go.json":"48ff011f3e171f977cfed2928272667e9361f13a508e5994f00f2241f6a23419","opencode.json":"882b039f64ef266da7b02d30aee9255c191e16f0600ef5494022bc1ef746f749","opengateway.json":"008c69b83d5fc98382e5463d77f701cdd87bb231c59830eabbbd453038d5a59f","openrouter.json":"4287f52f3e42a1873c09b72a9e6870c90718e0c71ed0b58c2f15da9e7271a529","qwen-token-plan-cn.json":"bd36a6d9cc2048deafacc85f7f0f2e51868c8b7aa09502e3e4e60e3e8b76f690","qwen-token-plan-individual.json":"35ffb0ba1a1ce3f68fc73262dd5e07fbcb5bf5911ad1e6e17bc0621e2894676a","qwen-token-plan.json":"ca27e49e62965221badead8e867b2007035c6b6a15a97c47219bd23d2c83c2b1","radius.json":"8214122385e8e4c4bc2e1d2a7d032f9e4c654da3bfd3ac8c5d4396e1b522e53a","together.json":"bbd7fad86cacfefe3d3d61f6638f22e9b6f5d575722fd1112a16aafe0485c46d","typesafe.json":"a4429a2f696d5cc5273969af61a7310bda07d6bdcd27bf9ac9c76c534cc43716","venice.json":"3604c88c46dd3e4151b833927c6f87bcc165d0999a4339b32db2ad74a9ff173f","vercel-ai-gateway.json":"b63765fc532a52b85b98d6b5afe8be3a3838b1907ae116cb25cf3d029bd70f0c","xai.json":"d7771a1450b21c1f6aee887ca6fea61b6e7b3fc13b4dee5cf72a71134fe623bc","xiaomi-token-plan-ams.json":"4dd8f838e21d7aa7caf3122d0b0b1e5a02a33bc012b96e6a75bd119334152d7a","xiaomi-token-plan-cn.json":"46414ff49973b95a0c14bd33e8992cc1938a50ce2c7cea9e2e5cc02de8f1df31","xiaomi-token-plan-sgp.json":"f5b3f66bf7bb7238592d4a9667cad983a008d7539a66e9896b02422622800935","xiaomi.json":"8b62c158d55d5a165f35cd72c1fc82ab89ff8e73f08960105fc41b822f236f69","zai-coding-cn.json":"2a7f59d15f588a97d218997883f2506dfc52d3ee320697d39f14213a050fc912","zai.json":"e9cc408e1756de8abc70148d63d89d11a63c65b7ef06ad0661cd1328e4209ba3"}}
|