@oh-my-pi/pi-ai 18.2.8 → 18.2.9

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 (45) hide show
  1. package/CHANGELOG.md +33 -18
  2. package/dist/types/auth-gateway/types.d.ts +5 -0
  3. package/dist/types/auth-storage.d.ts +35 -32
  4. package/dist/types/index.d.ts +1 -0
  5. package/dist/types/providers/amazon-bedrock.d.ts +7 -0
  6. package/dist/types/providers/claude-code-fingerprint.d.ts +22 -3
  7. package/dist/types/providers/google-gemini-cli.d.ts +0 -2
  8. package/dist/types/providers/openai-chat-server-schema.d.ts +2 -2
  9. package/dist/types/usage/claude-api.d.ts +22 -0
  10. package/dist/types/usage/claude-reset.d.ts +44 -0
  11. package/dist/types/usage.d.ts +111 -5
  12. package/dist/types/utils/schema/json-schema-validator.d.ts +5 -2
  13. package/dist/types/utils/tool-call-loop-guard.d.ts +1 -1
  14. package/package.json +6 -6
  15. package/src/auth/sqlite-credential-store.ts +44 -1
  16. package/src/auth-broker/remote-store.ts +6 -6
  17. package/src/auth-broker/wire-schemas.ts +14 -0
  18. package/src/auth-gateway/server.ts +4 -0
  19. package/src/auth-gateway/types.ts +5 -0
  20. package/src/auth-storage.ts +263 -140
  21. package/src/error/flags.ts +10 -0
  22. package/src/index.ts +1 -0
  23. package/src/providers/amazon-bedrock.ts +55 -5
  24. package/src/providers/anthropic.ts +45 -11
  25. package/src/providers/aws-credentials.ts +124 -11
  26. package/src/providers/claude-code-fingerprint.ts +55 -3
  27. package/src/providers/gitlab-duo.ts +20 -4
  28. package/src/providers/google-gemini-cli.ts +0 -8
  29. package/src/providers/google-shared.ts +1 -18
  30. package/src/providers/openai-chat-server-schema.ts +1 -1
  31. package/src/providers/openai-chat-server.ts +3 -1
  32. package/src/providers/openai-codex-responses.ts +27 -5
  33. package/src/providers/openai-completions.ts +122 -19
  34. package/src/providers/pi-native-server.ts +1 -0
  35. package/src/registry/oauth/anthropic.ts +2 -3
  36. package/src/stream.ts +13 -4
  37. package/src/usage/alibaba-token-plan.ts +7 -1
  38. package/src/usage/claude-api.ts +66 -0
  39. package/src/usage/claude-reset.ts +638 -0
  40. package/src/usage/claude.ts +37 -59
  41. package/src/usage/kimi.ts +32 -1
  42. package/src/usage.ts +52 -5
  43. package/src/utils/schema/json-schema-validator.ts +23 -10
  44. package/src/utils/tool-call-loop-guard.ts +2 -2
  45. package/src/utils/validation.ts +145 -50
@@ -4290,11 +4290,22 @@ async function openCodexSseEventStream(
4290
4290
  // an internal timeout stays retryable while an explicit abort fails fast.
4291
4291
  let clearPreResponseTimeout: (() => void) | undefined;
4292
4292
  const fetchAttempt: FetchImpl = async (input, init) => {
4293
+ let response: Response | undefined;
4293
4294
  try {
4294
- return await (fetchOverride ?? fetch)(input, init);
4295
+ response = await (fetchOverride ?? fetch)(input, init);
4296
+ return response;
4295
4297
  } finally {
4296
- clearPreResponseTimeout?.();
4297
- clearPreResponseTimeout = undefined;
4298
+ // A successful streaming body is governed by the iterator-level idle
4299
+ // watchdog, so disarm the pre-response guard the instant headers arrive.
4300
+ // Keep it armed for a non-2xx response: the error body is still
4301
+ // consumed under this deadline — by fetchWithRetry's
4302
+ // retry-status inspection and by CodexApiError.fromResponse — otherwise a
4303
+ // server that sends headers then stalls the body hangs the turn past every
4304
+ // configured first-event/idle deadline (issue #12664).
4305
+ if (!response || response.ok) {
4306
+ clearPreResponseTimeout?.();
4307
+ clearPreResponseTimeout = undefined;
4308
+ }
4298
4309
  }
4299
4310
  };
4300
4311
  const bodyJson = JSON.stringify(body);
@@ -4318,6 +4329,9 @@ async function openCodexSseEventStream(
4318
4329
  body: requestBody,
4319
4330
  signal,
4320
4331
  prepareInit: () => {
4332
+ // A retried non-2xx attempt leaves its guard armed for the retry-status
4333
+ // body read; disarm it before arming the next attempt's guard.
4334
+ clearPreResponseTimeout?.();
4321
4335
  const watchdog = armPreResponseTimeout(signal, firstEventTimeoutMs);
4322
4336
  clearPreResponseTimeout = watchdog.clear;
4323
4337
  return { signal: watchdog.signal };
@@ -4342,8 +4356,9 @@ async function openCodexSseEventStream(
4342
4356
  });
4343
4357
  response = await send(bodyJson);
4344
4358
  }
4345
- } finally {
4359
+ } catch (error) {
4346
4360
  clearPreResponseTimeout?.();
4361
+ throw error;
4347
4362
  }
4348
4363
  CODEX_DEBUG &&
4349
4364
  logger.debug("[codex] codex response", {
@@ -4354,7 +4369,14 @@ async function openCodexSseEventStream(
4354
4369
  cfRay: response.headers.get("cf-ray") || null,
4355
4370
  });
4356
4371
  if (!response.ok) {
4357
- throw await CodexApiError.fromResponse(response);
4372
+ // The pre-response guard is still armed for a non-2xx response; keep it live
4373
+ // across the error-body read so a stalled body is bounded, then disarm once
4374
+ // the read settles (issue #12664).
4375
+ try {
4376
+ throw await CodexApiError.fromResponse(response);
4377
+ } finally {
4378
+ clearPreResponseTimeout?.();
4379
+ }
4358
4380
  }
4359
4381
  updateCodexSessionMetadataFromHeaders(turnState, state, response.headers);
4360
4382
  if (!response.body) {
@@ -1,3 +1,4 @@
1
+ import { resolveModelPolicy } from "@oh-my-pi/pi-catalog/compat/resolve";
1
2
  import type { Effort } from "@oh-my-pi/pi-catalog/effort";
2
3
  import { resolveWireModelId } from "@oh-my-pi/pi-catalog/model-thinking";
3
4
  import { calculateCost } from "@oh-my-pi/pi-catalog/models";
@@ -133,6 +134,13 @@ type OpenAICompletionsDeltaWithReasoningDetails = ChatCompletionChunk.Choice["de
133
134
  reasoning_details?: unknown;
134
135
  };
135
136
 
137
+ type GeminiMessageThoughtSignatureField = "thinking_signature" | "thought_signature";
138
+
139
+ type GeminiMessageThoughtSignature = {
140
+ field: GeminiMessageThoughtSignatureField;
141
+ signature: string;
142
+ };
143
+
136
144
  type GeminiThoughtSignatureNamespace = "google" | "vertex";
137
145
 
138
146
  type GeminiThoughtSignatureExtraContent = Partial<
@@ -145,6 +153,11 @@ type OpenAICompletionsFunctionToolCall = ChatCompletionMessageFunctionToolCall &
145
153
 
146
154
  const GEMINI_THOUGHT_SIGNATURE_NAMESPACES: readonly GeminiThoughtSignatureNamespace[] = ["google", "vertex"];
147
155
 
156
+ const GEMINI_MESSAGE_THOUGHT_SIGNATURE_FIELDS: readonly GeminiMessageThoughtSignatureField[] = [
157
+ "thinking_signature",
158
+ "thought_signature",
159
+ ];
160
+
148
161
  function getGeminiThoughtSignatureExtraContent(value: unknown): GeminiThoughtSignatureExtraContent | undefined {
149
162
  if (typeof value !== "object" || value === null) return undefined;
150
163
  for (const namespace of GEMINI_THOUGHT_SIGNATURE_NAMESPACES) {
@@ -159,20 +172,66 @@ function getGeminiThoughtSignatureExtraContent(value: unknown): GeminiThoughtSig
159
172
  return undefined;
160
173
  }
161
174
 
162
- function parseGeminiThoughtSignatureExtraContent(
163
- thoughtSignature: string | undefined,
164
- ): GeminiThoughtSignatureExtraContent | undefined {
175
+ function getGeminiMessageThoughtSignature(value: unknown): GeminiMessageThoughtSignature | undefined {
176
+ if (typeof value !== "object" || value === null) return undefined;
177
+ for (const field of GEMINI_MESSAGE_THOUGHT_SIGNATURE_FIELDS) {
178
+ const signature = Reflect.get(value, field);
179
+ if (typeof signature === "string" && signature.length > 0) return { field, signature };
180
+ }
181
+ return undefined;
182
+ }
183
+
184
+ function parseStoredThoughtSignature(thoughtSignature: string | undefined): unknown {
165
185
  if (!thoughtSignature) return undefined;
166
186
  try {
167
- const parsed: unknown = JSON.parse(thoughtSignature);
168
- return getGeminiThoughtSignatureExtraContent(parsed);
187
+ return JSON.parse(thoughtSignature);
169
188
  } catch {
170
189
  return undefined;
171
190
  }
172
191
  }
173
192
 
193
+ // A single tool-call turn on an OpenAI-compatible Gemini wire can carry two
194
+ // independent signatures: a per-call one (`extra_content.google|vertex` or an
195
+ // encrypted `reasoning_details` entry) and a message-level `thinking_signature`
196
+ // / `thought_signature`. Both are stashed together on the originating tool
197
+ // call's `thoughtSignature` so persistence and replay preserve each field.
198
+ // `perCall` holds the raw extra_content object or reasoning detail; `message`
199
+ // holds the message-level signature in its wire shape.
200
+ type StoredGeminiSignature = {
201
+ perCall?: unknown;
202
+ message?: Partial<Record<GeminiMessageThoughtSignatureField, string>>;
203
+ };
204
+
205
+ // Reads the stored envelope, also accepting the legacy raw shapes emitted before
206
+ // the envelope existed (a bare extra_content object, reasoning detail, or
207
+ // message-level signature) so persisted history keeps replaying.
208
+ function normalizeStoredGeminiSignature(value: unknown): StoredGeminiSignature | undefined {
209
+ if (typeof value !== "object" || value === null) return undefined;
210
+ const perCall = Reflect.get(value, "perCall");
211
+ const envelopeMessage = getGeminiMessageThoughtSignature(Reflect.get(value, "message"));
212
+ if (perCall !== undefined || envelopeMessage) {
213
+ const normalized: StoredGeminiSignature = {};
214
+ if (perCall !== undefined) normalized.perCall = perCall;
215
+ if (envelopeMessage) normalized.message = { [envelopeMessage.field]: envelopeMessage.signature };
216
+ return normalized;
217
+ }
218
+ const legacyMessage = getGeminiMessageThoughtSignature(value);
219
+ if (legacyMessage) return { message: { [legacyMessage.field]: legacyMessage.signature } };
220
+ return { perCall: value };
221
+ }
222
+
223
+ // Merges a new per-call or message-level signature into whatever is already
224
+ // stored, so a later message-level signature never clobbers an earlier per-call
225
+ // one (and vice versa).
226
+ function mergeStoredGeminiSignature(existing: string | undefined, update: StoredGeminiSignature): string {
227
+ const merged = normalizeStoredGeminiSignature(parseStoredThoughtSignature(existing)) ?? {};
228
+ if (update.perCall !== undefined) merged.perCall = update.perCall;
229
+ if (update.message) merged.message = update.message;
230
+ return JSON.stringify(merged);
231
+ }
232
+
174
233
  type OpenAICompletionsAssistantMessageParam = ChatCompletionAssistantMessageParam &
175
- Partial<Record<OpenAICompletionsReasoningField, string>> & {
234
+ Partial<Record<OpenAICompletionsReasoningField | GeminiMessageThoughtSignatureField, string>> & {
176
235
  reasoning_details?: unknown[];
177
236
  };
178
237
 
@@ -919,6 +978,7 @@ const streamOpenAICompletionsOnce = (
919
978
  }
920
979
  };
921
980
  let currentBlock: OpenAIStreamBlock | undefined;
981
+ let messageThoughtSignature: GeminiMessageThoughtSignature | undefined;
922
982
  const blockIndex = (block: OpenAIStreamBlock | undefined): number => {
923
983
  if (!block) return Math.max(0, output.content.length - 1);
924
984
  return output.content.indexOf(block);
@@ -1347,7 +1407,11 @@ const streamOpenAICompletionsOnce = (
1347
1407
  if (toolCall.id) block.id = toolCall.id;
1348
1408
  if (incomingName) block.name = incomingName;
1349
1409
  const extraContent = getGeminiThoughtSignatureExtraContent(Reflect.get(toolCall, "extra_content"));
1350
- if (extraContent) block.thoughtSignature = JSON.stringify(extraContent);
1410
+ if (extraContent) {
1411
+ block.thoughtSignature = mergeStoredGeminiSignature(block.thoughtSignature, {
1412
+ perCall: extraContent,
1413
+ });
1414
+ }
1351
1415
  let delta = "";
1352
1416
  // The OpenAI SDK types `function.arguments` as a JSON string, but MiniMax-compatible
1353
1417
  // hosts stream a fully-formed object instead. Model both shapes so the branches below
@@ -1411,11 +1475,29 @@ const streamOpenAICompletionsOnce = (
1411
1475
  b => b.type === "toolCall" && b.id === detailObject.id,
1412
1476
  ) as ToolCall | undefined;
1413
1477
  if (matchingToolCall) {
1414
- matchingToolCall.thoughtSignature = JSON.stringify(detailObject);
1478
+ matchingToolCall.thoughtSignature = mergeStoredGeminiSignature(
1479
+ matchingToolCall.thoughtSignature,
1480
+ { perCall: detailObject },
1481
+ );
1415
1482
  }
1416
1483
  }
1417
1484
  }
1418
1485
  }
1486
+
1487
+ const incomingMessageThoughtSignature = getGeminiMessageThoughtSignature(choice.delta);
1488
+ if (incomingMessageThoughtSignature) messageThoughtSignature = incomingMessageThoughtSignature;
1489
+ if (messageThoughtSignature) {
1490
+ for (const block of output.content) {
1491
+ if (block.type !== "toolCall") continue;
1492
+ block.thoughtSignature = mergeStoredGeminiSignature(block.thoughtSignature, {
1493
+ message: {
1494
+ [messageThoughtSignature.field]: messageThoughtSignature.signature,
1495
+ },
1496
+ });
1497
+ messageThoughtSignature = undefined;
1498
+ break;
1499
+ }
1500
+ }
1419
1501
  }
1420
1502
 
1421
1503
  // If usage arrived on the finish chunk without cache-read fields,
@@ -1542,16 +1624,33 @@ const streamOpenAICompletionsOnce = (
1542
1624
  return stream;
1543
1625
  };
1544
1626
 
1627
+ /**
1628
+ * Custom APIs deliberately have no catalog compat type. Once an extension
1629
+ * explicitly delegates to this streamer, resolve the OpenAI wire policy on a
1630
+ * request-local clone while preserving the custom API id on the original model.
1631
+ */
1632
+ function resolveOpenAICompletionsCompat(model: Model<"openai-completions">): Model<"openai-completions"> {
1633
+ if (model.compat !== undefined) return model;
1634
+ const compat = resolveModelPolicy({
1635
+ ...model,
1636
+ api: "openai-completions",
1637
+ compat: model.compatConfig,
1638
+ }).compat;
1639
+ return { ...model, compat };
1640
+ }
1641
+
1545
1642
  /**
1546
1643
  * Retries benign empty completions and transient provider failures only before
1547
1644
  * assistant output commits the attempt.
1548
1645
  */
1549
- export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (model, context, options) =>
1550
- withReplaySafeStreamRetry(model, context, options, streamOpenAICompletionsOnce, {
1646
+ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (model, context, options) => {
1647
+ const resolvedModel = resolveOpenAICompletionsCompat(model);
1648
+ return withReplaySafeStreamRetry(resolvedModel, context, options, streamOpenAICompletionsOnce, {
1551
1649
  retryEmptyCompletion: true,
1552
1650
  retryProviderErrors: true,
1553
1651
  maxProviderErrorRetries: 1,
1554
1652
  });
1653
+ };
1555
1654
 
1556
1655
  function createRequestSetup(
1557
1656
  model: Model<"openai-completions">,
@@ -2350,19 +2449,23 @@ export function convertMessages(
2350
2449
  arguments: serializeToolArguments(tc.arguments),
2351
2450
  },
2352
2451
  };
2353
- const extraContent = parseGeminiThoughtSignatureExtraContent(tc.thoughtSignature);
2452
+ const stored = normalizeStoredGeminiSignature(parseStoredThoughtSignature(tc.thoughtSignature));
2453
+ const extraContent = getGeminiThoughtSignatureExtraContent(stored?.perCall);
2354
2454
  if (extraContent) replayedToolCall.extra_content = extraContent;
2355
2455
  return replayedToolCall;
2356
2456
  });
2457
+ for (const toolCall of toolCalls) {
2458
+ const stored = normalizeStoredGeminiSignature(parseStoredThoughtSignature(toolCall.thoughtSignature));
2459
+ const messageSignature = getGeminiMessageThoughtSignature(stored?.message);
2460
+ if (!messageSignature) continue;
2461
+ assistantMsg[messageSignature.field] = messageSignature.signature;
2462
+ break;
2463
+ }
2357
2464
  const reasoningDetails = toolCalls.flatMap(tc => {
2358
- const thoughtSignature = tc.thoughtSignature;
2359
- if (!thoughtSignature) return [];
2360
- try {
2361
- const parsed: unknown = JSON.parse(thoughtSignature);
2362
- return getGeminiThoughtSignatureExtraContent(parsed) ? [] : [parsed];
2363
- } catch {
2364
- return [];
2365
- }
2465
+ const stored = normalizeStoredGeminiSignature(parseStoredThoughtSignature(tc.thoughtSignature));
2466
+ const perCall = stored?.perCall;
2467
+ if (perCall === undefined || getGeminiThoughtSignatureExtraContent(perCall)) return [];
2468
+ return [perCall];
2366
2469
  });
2367
2470
  if (reasoningDetails.length > 0) {
2368
2471
  assistantMsg.reasoning_details = reasoningDetails;
@@ -69,6 +69,7 @@ const ALLOWED_OPTION_KEYS: ReadonlySet<keyof SimpleStreamOptions> = new Set([
69
69
  "streamIdleTimeoutMs",
70
70
  "reasoning",
71
71
  "disableReasoning",
72
+ "forceReasoningOff",
72
73
  "hideThinkingSummary",
73
74
  "thinkingBudgets",
74
75
  "toolChoice",
@@ -7,14 +7,13 @@
7
7
  */
8
8
 
9
9
  import * as AIError from "../../error";
10
- import { claudeCodeVersion } from "../../providers/claude-code-fingerprint";
10
+ import { getClaudeCodeVersion } from "../../providers/claude-code-fingerprint";
11
11
  import type { FetchImpl } from "../../types";
12
12
  import type { AfterExchangeHook } from "../hooks/types";
13
13
  import type { OAuthCredentials } from "./types";
14
14
 
15
15
  const BOOTSTRAP_URL = "https://api.anthropic.com/api/claude_cli/bootstrap";
16
16
  const CLAUDE_CODE_BOOTSTRAP_MODEL = "claude-opus-4-8";
17
- const CLAUDE_CODE_BOOTSTRAP_USER_AGENT = `claude-code/${claudeCodeVersion}`;
18
17
 
19
18
  export { ANTHROPIC_OAUTH_GRANT_TTL_MS } from "./anthropic-constants";
20
19
 
@@ -56,7 +55,7 @@ export async function fetchAnthropicBootstrapIdentity(
56
55
  Accept: "application/json, text/plain, */*",
57
56
  Authorization: `Bearer ${accessToken}`,
58
57
  "Content-Type": "application/json",
59
- "User-Agent": CLAUDE_CODE_BOOTSTRAP_USER_AGENT,
58
+ "User-Agent": `claude-code/${getClaudeCodeVersion()}`,
60
59
  "anthropic-beta": "oauth-2025-04-20",
61
60
  },
62
61
  signal: AbortSignal.timeout(30_000),
package/src/stream.ts CHANGED
@@ -1841,7 +1841,7 @@ function resolveBedrockThinkingBudget(
1841
1841
  model: Model<"bedrock-converse-stream">,
1842
1842
  options?: SimpleStreamOptions,
1843
1843
  ): { budget: number; level: Effort } | null {
1844
- if (!options?.reasoning || !model.reasoning) return null;
1844
+ if (!options?.reasoning || !model.reasoning || options.disableReasoning || options.forceReasoningOff) return null;
1845
1845
  const level = requireSupportedEffort(model, options.reasoning);
1846
1846
  const budget = options.thinkingBudgets?.[level] ?? BEDROCK_CLAUDE_THINKING[level];
1847
1847
  return { budget, level };
@@ -2167,7 +2167,11 @@ function mapOptionsForApi<TApi extends Api>(
2167
2167
  case "bedrock-converse-stream": {
2168
2168
  const bedrockBase: BedrockOptions = {
2169
2169
  ...base,
2170
- reasoning: options?.reasoning,
2170
+ // Explicit reasoning-off must fold here like the anthropic-messages
2171
+ // branch: the provider gates thinking only on `reasoning`, and the
2172
+ // budget path below must not inflate a capped request for thinking
2173
+ // that was turned off.
2174
+ reasoning: options?.disableReasoning || options?.forceReasoningOff ? undefined : options?.reasoning,
2171
2175
  thinkingBudgets: options?.thinkingBudgets,
2172
2176
  toolChoice: mapAnthropicToolChoice(options?.toolChoice),
2173
2177
  thinkingDisplay: options?.hideThinkingSummary ? "omitted" : undefined,
@@ -2212,6 +2216,9 @@ function mapOptionsForApi<TApi extends Api>(
2212
2216
  openrouterVariant: options?.openrouterVariant,
2213
2217
  maxTokensExplicit: rawOptions?.maxTokens !== undefined,
2214
2218
  disableReasoning: options?.disableReasoning,
2219
+ // Forwarded, not folded: the Responses record reads both flags
2220
+ // itself (`applyResponsesCompatPolicy`).
2221
+ forceReasoningOff: options?.forceReasoningOff,
2215
2222
  textVerbosity: options?.textVerbosity,
2216
2223
  promptCache: options?.promptCache,
2217
2224
  statefulResponses: options?.statefulResponses,
@@ -2220,7 +2227,8 @@ function mapOptionsForApi<TApi extends Api>(
2220
2227
  return castApi<"openai-completions">({
2221
2228
  ...base,
2222
2229
  reasoning: resolveOpenAiReasoningEffort(model, options),
2223
- disableReasoning: options?.disableReasoning,
2230
+ // `OpenAICompletionsOptions` carries no forceReasoningOff; fold it.
2231
+ disableReasoning: options?.disableReasoning || options?.forceReasoningOff,
2224
2232
  toolChoice: mapOpenAiToolChoice(options?.toolChoice),
2225
2233
  serviceTier: options?.serviceTier,
2226
2234
  openrouterVariant: options?.openrouterVariant,
@@ -2233,7 +2241,8 @@ function mapOptionsForApi<TApi extends Api>(
2233
2241
  return castApi<"openai-completions">({
2234
2242
  ...base,
2235
2243
  reasoning: resolveOpenAiReasoningEffort(model, options),
2236
- disableReasoning: options?.disableReasoning,
2244
+ // `OpenAICompletionsOptions` carries no forceReasoningOff; fold it.
2245
+ disableReasoning: options?.disableReasoning || options?.forceReasoningOff,
2237
2246
  toolChoice: mapOpenAiToolChoice(options?.toolChoice),
2238
2247
  serviceTier: options?.serviceTier,
2239
2248
  openrouterVariant: options?.openrouterVariant,
@@ -46,7 +46,6 @@ const CHINA_CONSOLE = {
46
46
  protocol: "V2",
47
47
  console: "ONE_CONSOLE",
48
48
  productCode: "p_efm",
49
- switchAgent: 12608464,
50
49
  switchUserType: 3,
51
50
  domain: "bailian.console.aliyun.com",
52
51
  consoleSite: "BAILIAN_ALIYUN",
@@ -221,6 +220,13 @@ async function fetchAlibabaTokenPlanUsage(
221
220
  ctx.logger?.warn("Alibaba Token Plan usage response invalid", { provider: PROVIDER });
222
221
  return null;
223
222
  }
223
+ if (payload.data.success === false) {
224
+ ctx.logger?.warn("Alibaba Token Plan usage request rejected", {
225
+ provider: PROVIDER,
226
+ errorCode: typeof payload.data.errorCode === "string" ? payload.data.errorCode : "unknown",
227
+ });
228
+ return null;
229
+ }
224
230
  const responseData = unwrapGatewayData(payload.data);
225
231
  const limits = [
226
232
  buildLimit(
@@ -0,0 +1,66 @@
1
+ import { getClaudeCodeUserAgent } from "../providers/claude-code-fingerprint";
2
+
3
+ /** Canonical host for Claude's first-party account API. */
4
+ export const DEFAULT_CLAUDE_API_BASE_URL = "https://api.anthropic.com";
5
+ /** Canonical subscription usage and profile endpoint. */
6
+ export const DEFAULT_CLAUDE_OAUTH_BASE_URL = `${DEFAULT_CLAUDE_API_BASE_URL}/api/oauth`;
7
+ /** OAuth protocol beta required by Claude's account routes. */
8
+ export const CLAUDE_OAUTH_BETA = "oauth-2025-04-20";
9
+
10
+ /**
11
+ * Normalize Messages (`/v1`) and OAuth (`/api/oauth`) endpoints to the common
12
+ * API root used by both usage and reset routes. Custom path prefixes survive.
13
+ */
14
+ export function normalizeClaudeApiBaseUrl(baseUrl?: string): string {
15
+ if (!baseUrl?.trim()) return DEFAULT_CLAUDE_API_BASE_URL;
16
+ let url: URL;
17
+ try {
18
+ url = new URL(baseUrl.trim());
19
+ } catch {
20
+ return DEFAULT_CLAUDE_API_BASE_URL;
21
+ }
22
+ let path = url.pathname.replace(/\/+$/, "");
23
+ if (path === "/") path = "";
24
+ const lower = path.toLowerCase();
25
+ if (lower.endsWith("/api/oauth")) {
26
+ path = path.slice(0, -"/api/oauth".length);
27
+ } else if (lower.endsWith("/v1")) {
28
+ path = path.slice(0, -"/v1".length);
29
+ }
30
+ return `${url.origin}${path}`;
31
+ }
32
+
33
+ /** Resolve the OAuth API base while retaining a configured proxy path prefix. */
34
+ export function claudeOAuthBaseUrl(baseUrl?: string): string {
35
+ return `${normalizeClaudeApiBaseUrl(baseUrl)}/api/oauth`;
36
+ }
37
+
38
+ /** Configured OAuth endpoint followed by canonical fallback, without duplicates. */
39
+ export function claudeOAuthBaseUrls(baseUrl?: string): readonly string[] {
40
+ const configured = claudeOAuthBaseUrl(baseUrl);
41
+ return configured === DEFAULT_CLAUDE_OAUTH_BASE_URL
42
+ ? [DEFAULT_CLAUDE_OAUTH_BASE_URL]
43
+ : [configured, DEFAULT_CLAUDE_OAUTH_BASE_URL];
44
+ }
45
+
46
+ /** Resolve a first-party account route against the configured Claude API root. */
47
+ export function claudeApiUrl(baseUrl: string | undefined, path: string): string {
48
+ const suffix = path.startsWith("/") ? path : `/${path}`;
49
+ return `${normalizeClaudeApiBaseUrl(baseUrl)}${suffix}`;
50
+ }
51
+
52
+ /** Shared OAuth and CLI identity headers for Claude usage, profile, and resets. */
53
+ export function buildClaudeOAuthHeaders(
54
+ accessToken: string,
55
+ options: { beta?: string; json?: boolean } = {},
56
+ ): Record<string, string> {
57
+ return {
58
+ accept: "application/json, text/plain, */*",
59
+ "accept-encoding": "gzip, compress, deflate, br",
60
+ "anthropic-beta": options.beta ?? CLAUDE_OAUTH_BETA,
61
+ ...(options.json === false ? {} : { "content-type": "application/json" }),
62
+ connection: "keep-alive",
63
+ "user-agent": getClaudeCodeUserAgent(),
64
+ authorization: `Bearer ${accessToken}`,
65
+ };
66
+ }