@oh-my-pi/pi-ai 18.2.6 → 18.2.7

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 (49) hide show
  1. package/CHANGELOG.md +13 -1
  2. package/THIRD-PARTY-NOTICES.txt +0 -37
  3. package/dist/types/auth-retry.d.ts +2 -0
  4. package/dist/types/auth-storage.d.ts +2 -2
  5. package/dist/types/error/classes.d.ts +4 -1
  6. package/dist/types/error/flags.d.ts +12 -4
  7. package/dist/types/index.d.ts +14 -12
  8. package/dist/types/judgment/types.d.ts +5 -2
  9. package/dist/types/judgment/typesafe.d.ts +16 -22
  10. package/dist/types/providers/anthropic-compaction.d.ts +10 -0
  11. package/dist/types/providers/anthropic-identity.d.ts +18 -0
  12. package/dist/types/providers/anthropic-state.d.ts +13 -0
  13. package/dist/types/providers/anthropic.d.ts +6 -59
  14. package/dist/types/providers/gitlab-duo.d.ts +0 -1
  15. package/dist/types/providers/kimi.d.ts +1 -5
  16. package/dist/types/providers/openai-codex-attestation.d.ts +6 -0
  17. package/dist/types/providers/openai-codex-compaction.d.ts +6 -0
  18. package/dist/types/providers/openai-codex-responses.d.ts +3 -27
  19. package/dist/types/providers/openai-codex-transport.d.ts +7 -0
  20. package/dist/types/providers/openai-shared.d.ts +0 -9
  21. package/dist/types/providers/register-builtins.d.ts +20 -22
  22. package/dist/types/providers/synthetic.d.ts +1 -5
  23. package/package.json +6 -6
  24. package/src/auth-retry.ts +3 -0
  25. package/src/auth-storage.ts +28 -22
  26. package/src/error/auth-classify.ts +10 -2
  27. package/src/error/classes.ts +23 -3
  28. package/src/error/flags.ts +51 -16
  29. package/src/index.ts +15 -12
  30. package/src/judgment/types.ts +6 -3
  31. package/src/judgment/typesafe.ts +53 -48
  32. package/src/provider-session-state.ts +1 -1
  33. package/src/providers/anthropic-compaction.ts +54 -0
  34. package/src/providers/anthropic-identity.ts +136 -0
  35. package/src/providers/anthropic-state.ts +55 -0
  36. package/src/providers/anthropic.ts +39 -296
  37. package/src/providers/bedrock-mantle.ts +1 -1
  38. package/src/providers/gitlab-duo.ts +0 -4
  39. package/src/providers/kimi.ts +1 -8
  40. package/src/providers/openai-codex-attestation.ts +19 -0
  41. package/src/providers/openai-codex-compaction.ts +18 -0
  42. package/src/providers/openai-codex-responses.ts +9 -68
  43. package/src/providers/openai-codex-transport.ts +18 -0
  44. package/src/providers/openai-shared.ts +1 -10
  45. package/src/providers/register-builtins.ts +113 -320
  46. package/src/providers/synthetic.ts +1 -8
  47. package/src/registry/cloudflare-ai-gateway.ts +1 -1
  48. package/src/stream.ts +7 -15
  49. package/src/utils/anthropic-auth.ts +3 -6
@@ -0,0 +1,136 @@
1
+ import * as nodeCrypto from "node:crypto";
2
+ import { getInstallId } from "@oh-my-pi/pi-utils";
3
+ import { claudeToolPrefix } from "./claude-code-fingerprint";
4
+
5
+ const CLAUDE_CLOAKING_USER_ID_REGEX =
6
+ /^user_[0-9a-fA-F]{64}_account_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}_session_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
7
+
8
+ /** Whether a metadata id uses Claude Code's legacy cloaking shape. */
9
+ export function isClaudeCloakingUserId(userId: string): boolean {
10
+ return CLAUDE_CLOAKING_USER_ID_REGEX.test(userId);
11
+ }
12
+
13
+ function isClaudeJsonUserId(userId: string): boolean {
14
+ if (userId.length === 0 || userId[0] !== "{") return false;
15
+ let parsed: unknown;
16
+ try {
17
+ parsed = JSON.parse(userId);
18
+ } catch {
19
+ return false;
20
+ }
21
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) || !("session_id" in parsed)) return false;
22
+ return typeof parsed.session_id === "string" && parsed.session_id.length > 0;
23
+ }
24
+
25
+ /** Extract a stable session id from a supported Anthropic metadata user id. */
26
+ export function extractClaudeMetadataSessionId(userId: unknown): string | undefined {
27
+ if (typeof userId !== "string") return undefined;
28
+ if (isClaudeCloakingUserId(userId)) {
29
+ return userId.slice(userId.lastIndexOf("_session_") + "_session_".length);
30
+ }
31
+ if (userId.length === 0 || userId[0] !== "{") return undefined;
32
+ let parsed: unknown;
33
+ try {
34
+ parsed = JSON.parse(userId);
35
+ } catch {
36
+ return undefined;
37
+ }
38
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) || !("session_id" in parsed)) return undefined;
39
+ const sessionId = parsed.session_id;
40
+ return typeof sessionId === "string" && sessionId.length > 0 ? sessionId : undefined;
41
+ }
42
+
43
+ /** Generate a legacy Claude Code cloaking metadata id. */
44
+ export function generateClaudeCloakingUserId(): string {
45
+ const userHash = nodeCrypto.randomBytes(32).toString("hex");
46
+ const accountId = nodeCrypto.randomUUID().toLowerCase();
47
+ const sessionId = nodeCrypto.randomUUID().toLowerCase();
48
+ return `user_${userHash}_account_${accountId}_session_${sessionId}`;
49
+ }
50
+
51
+ const CLAUDE_DEVICE_ID_INSTALL_HASH_DOMAIN = "omp-claude-device-id-v1:";
52
+ const CLAUDE_DEVICE_ID_ACCOUNT_HASH_DOMAIN = "omp-claude-device-id-v2";
53
+
54
+ /** Derive the stable Claude device id for an installation and optional account. */
55
+ export function deriveClaudeDeviceId(installId: string, accountId?: string): string {
56
+ const hash = nodeCrypto.createHash("sha256");
57
+ if (accountId && accountId.length > 0) {
58
+ return hash
59
+ .update(CLAUDE_DEVICE_ID_ACCOUNT_HASH_DOMAIN)
60
+ .update("\0")
61
+ .update(installId)
62
+ .update("\0")
63
+ .update(accountId)
64
+ .digest("hex");
65
+ }
66
+ return hash.update(CLAUDE_DEVICE_ID_INSTALL_HASH_DOMAIN).update(installId).digest("hex");
67
+ }
68
+
69
+ /** Read a non-empty string from provider metadata. */
70
+ export function readAnthropicMetadataString(
71
+ metadata: Record<string, unknown> | undefined,
72
+ key: string,
73
+ ): string | undefined {
74
+ const value = metadata?.[key];
75
+ return typeof value === "string" && value.length > 0 ? value : undefined;
76
+ }
77
+
78
+ /** Resolve the account id aliases accepted in Anthropic request metadata. */
79
+ export function readAnthropicMetadataAccountId(metadata: Record<string, unknown> | undefined): string | undefined {
80
+ return (
81
+ readAnthropicMetadataString(metadata, "account_uuid") ??
82
+ readAnthropicMetadataString(metadata, "accountId") ??
83
+ readAnthropicMetadataString(metadata, "account_id")
84
+ );
85
+ }
86
+
87
+ function deriveClaudeDeviceIdFromInstallId(accountId?: string): string {
88
+ return deriveClaudeDeviceId(getInstallId(), accountId);
89
+ }
90
+
91
+ function generateClaudeJsonUserId(sessionId?: string, accountId?: string): string {
92
+ const userId: Record<string, string> = {
93
+ device_id: deriveClaudeDeviceIdFromInstallId(accountId),
94
+ session_id: sessionId ?? nodeCrypto.randomUUID().toLowerCase(),
95
+ };
96
+ if (accountId && accountId.length > 0) userId.account_uuid = accountId;
97
+ return JSON.stringify(userId);
98
+ }
99
+
100
+ /** Resolve the provider-facing metadata user id for API-key or OAuth requests. */
101
+ export function resolveAnthropicMetadataUserId(
102
+ userId: unknown,
103
+ isOAuthToken: boolean,
104
+ sessionId?: string,
105
+ accountId?: string,
106
+ ): string | undefined {
107
+ if (typeof userId === "string") {
108
+ if (!isOAuthToken || isClaudeCloakingUserId(userId) || isClaudeJsonUserId(userId)) {
109
+ return userId;
110
+ }
111
+ }
112
+
113
+ if (!isOAuthToken) return undefined;
114
+ return generateClaudeJsonUserId(sessionId, accountId);
115
+ }
116
+
117
+ const ANTHROPIC_BUILTIN_TOOL_NAMES: Record<string, true> = {
118
+ web_search: true,
119
+ code_execution: true,
120
+ text_editor: true,
121
+ computer: true,
122
+ };
123
+
124
+ /** Apply Claude Code's wire-only tool prefix. */
125
+ export const applyClaudeToolPrefix = (name: string): string => {
126
+ if (!claudeToolPrefix) return name;
127
+ if (Object.hasOwn(ANTHROPIC_BUILTIN_TOOL_NAMES, name.toLowerCase())) return name;
128
+ return `${claudeToolPrefix}${name}`;
129
+ };
130
+
131
+ /** Remove one Claude Code wire-only tool prefix. */
132
+ export const stripClaudeToolPrefix = (name: string): string => {
133
+ if (!claudeToolPrefix) return name;
134
+ if (!name.toLowerCase().startsWith(claudeToolPrefix.toLowerCase())) return name;
135
+ return name.slice(claudeToolPrefix.length);
136
+ };
@@ -0,0 +1,55 @@
1
+ import { isOfficialAnthropicApiUrl } from "@oh-my-pi/pi-catalog/compat/anthropic";
2
+ import { $env } from "@oh-my-pi/pi-utils";
3
+ import type { Api, Model, ProviderSessionState } from "../types";
4
+ import { isFoundryEnabled } from "../utils/foundry";
5
+
6
+ /** Root key for Anthropic's per-session provider state. */
7
+ export const ANTHROPIC_PROVIDER_SESSION_STATE_KEY = "anthropic-messages";
8
+
9
+ /** Normalize an Anthropic base URL to its origin path without `/v1`. */
10
+ export function normalizeAnthropicBaseUrl(baseUrl?: string): string | undefined {
11
+ const trimmed = baseUrl?.trim();
12
+ if (!trimmed) return undefined;
13
+ const withoutTrailingSlashes = trimmed.replace(/\/+$/, "");
14
+ return withoutTrailingSlashes.endsWith("/v1") ? withoutTrailingSlashes.slice(0, -3) : withoutTrailingSlashes;
15
+ }
16
+
17
+ /** Resolve the endpoint for a first-party Anthropic model without loading its provider implementation. */
18
+ export function resolveDirectAnthropicBaseUrl(model: Model<Api>): string {
19
+ if (isFoundryEnabled()) {
20
+ const foundryBaseUrl = normalizeAnthropicBaseUrl($env.FOUNDRY_BASE_URL);
21
+ if (foundryBaseUrl) return foundryBaseUrl;
22
+ }
23
+ const configured = normalizeAnthropicBaseUrl(model.baseUrl);
24
+ if (configured && !isOfficialAnthropicApiUrl(configured)) return configured;
25
+ return normalizeAnthropicBaseUrl($env.ANTHROPIC_BASE_URL) ?? configured ?? "https://api.anthropic.com";
26
+ }
27
+
28
+ /** Build the endpoint-and-model-scoped Anthropic session-state key. */
29
+ export function anthropicProviderSessionStateKey(baseUrl: string, modelId: string): string {
30
+ return `${ANTHROPIC_PROVIDER_SESSION_STATE_KEY}:${baseUrl}\u0000${modelId}`;
31
+ }
32
+
33
+ /** Re-arm fast mode across materialized Anthropic session-state entries. */
34
+ export function clearAnthropicFastModeFallback(
35
+ providerSessionState: Map<string, ProviderSessionState> | undefined,
36
+ ): void {
37
+ if (!providerSessionState) return;
38
+ const prefix = `${ANTHROPIC_PROVIDER_SESSION_STATE_KEY}:`;
39
+ for (const [key, value] of providerSessionState) {
40
+ if (key !== ANTHROPIC_PROVIDER_SESSION_STATE_KEY && !key.startsWith(prefix)) continue;
41
+ Object.assign(value, { fastModeDisabled: false });
42
+ }
43
+ }
44
+
45
+ /** Inspect the direct model's fast-mode fallback without materializing state. */
46
+ export function isAnthropicFastModeFallbackDisabled(
47
+ providerSessionState: Map<string, ProviderSessionState> | undefined,
48
+ model: Model<Api>,
49
+ ): boolean {
50
+ if (!providerSessionState || model.provider !== "anthropic" || model.api !== "anthropic-messages") return false;
51
+ const baseUrl = resolveDirectAnthropicBaseUrl(model);
52
+ const key = anthropicProviderSessionStateKey(baseUrl, model.id);
53
+ const state = providerSessionState.get(key);
54
+ return state !== undefined && "fastModeDisabled" in state && state.fastModeDisabled === true;
55
+ }
@@ -10,7 +10,6 @@ import { isAnthropicOAuthToken } from "@oh-my-pi/pi-catalog/utils";
10
10
  import { parseGitHubCopilotApiKey } from "@oh-my-pi/pi-catalog/wire/github-copilot";
11
11
  import {
12
12
  $env,
13
- getInstallId,
14
13
  isEnoent,
15
14
  logger,
16
15
  parseJsonWithRepair,
@@ -129,6 +128,43 @@ import { getOpenAIPromptCacheKey } from "./openai-shared";
129
128
  import { applyInferenceHeaders } from "./inference-headers";
130
129
  import { redactSensitiveCredentials, transformMessages } from "./transform-messages";
131
130
  import { NON_VISION_IMAGE_PLACEHOLDER } from "./vision-guard";
131
+ import {
132
+ injectedClientBaseUrl,
133
+ resolvesToOfficialAnthropicEndpoint,
134
+ supportsAnthropicCompaction,
135
+ supportsAnthropicCompactionOnClient,
136
+ } from "./anthropic-compaction";
137
+ import {
138
+ applyClaudeToolPrefix,
139
+ deriveClaudeDeviceId,
140
+ extractClaudeMetadataSessionId,
141
+ generateClaudeCloakingUserId,
142
+ isClaudeCloakingUserId,
143
+ readAnthropicMetadataAccountId,
144
+ readAnthropicMetadataString,
145
+ resolveAnthropicMetadataUserId,
146
+ stripClaudeToolPrefix,
147
+ } from "./anthropic-identity";
148
+ import {
149
+ anthropicProviderSessionStateKey,
150
+ clearAnthropicFastModeFallback,
151
+ isAnthropicFastModeFallbackDisabled,
152
+ normalizeAnthropicBaseUrl,
153
+ resolveDirectAnthropicBaseUrl,
154
+ } from "./anthropic-state";
155
+
156
+ export {
157
+ applyClaudeToolPrefix,
158
+ clearAnthropicFastModeFallback,
159
+ deriveClaudeDeviceId,
160
+ generateClaudeCloakingUserId,
161
+ isAnthropicFastModeFallbackDisabled,
162
+ isClaudeCloakingUserId,
163
+ normalizeAnthropicBaseUrl,
164
+ resolveAnthropicMetadataUserId,
165
+ stripClaudeToolPrefix,
166
+ };
167
+ export { resolvesToOfficialAnthropicEndpoint, supportsAnthropicCompaction, supportsAnthropicCompactionOnClient };
132
168
 
133
169
  export type AnthropicHeaderOptions = {
134
170
  apiKey: string;
@@ -144,15 +180,6 @@ export type AnthropicHeaderOptions = {
144
180
  allowAnthropicHeaderOverrides?: boolean;
145
181
  };
146
182
 
147
- export function normalizeAnthropicBaseUrl(baseUrl?: string): string | undefined {
148
- const trimmed = baseUrl?.trim();
149
- if (!trimmed) {
150
- return undefined;
151
- }
152
- const withoutTrailingSlashes = trimmed.replace(/\/+$/, "");
153
- return withoutTrailingSlashes.endsWith("/v1") ? withoutTrailingSlashes.slice(0, -3) : withoutTrailingSlashes;
154
- }
155
-
156
183
  // Build deduplicated beta header string
157
184
  export function buildBetaHeader(baseBetas: readonly string[], extraBetas: readonly string[]): string {
158
185
  const seen = new Set<string>();
@@ -416,8 +443,6 @@ type AnthropicOutputConfig = NonNullable<MessageCreateParamsStreaming["output_co
416
443
  const ANTHROPIC_STOP_SEQUENCES_MAX = 4;
417
444
  let warnedStopSequencesTrim = false;
418
445
 
419
- const ANTHROPIC_PROVIDER_SESSION_STATE_KEY = "anthropic-messages";
420
-
421
446
  /**
422
447
  * A mid-conversation `role: "system"` message omp inserts at a fixed slot in
423
448
  * the wire history so top-level `tools` and `output_config.effort` can stay
@@ -504,18 +529,6 @@ function createAnthropicProviderSessionState(): AnthropicProviderSessionState {
504
529
  return state;
505
530
  }
506
531
 
507
- /**
508
- * Key the sticky strict-tools / fast-mode learning per endpoint+model. A
509
- * grammar-too-large 400 or a fast-mode rejection is specific to the model (its
510
- * tool grammar / entitlement) and the endpoint (direct Anthropic vs a gateway /
511
- * Foundry / Bedrock proxy), so it MUST NOT bleed onto unrelated anthropic-messages
512
- * requests in the same session. NUL separates the two components so neither can
513
- * forge the boundary.
514
- */
515
- function anthropicProviderSessionStateKey(baseUrl: string, modelId: string): string {
516
- return `${ANTHROPIC_PROVIDER_SESSION_STATE_KEY}:${baseUrl}\u0000${modelId}`;
517
- }
518
-
519
532
  function getAnthropicProviderSessionState(
520
533
  providerSessionState: Map<string, ProviderSessionState> | undefined,
521
534
  baseUrl: string,
@@ -534,41 +547,6 @@ function getAnthropicProviderSessionState(
534
547
  return created;
535
548
  }
536
549
 
537
- /**
538
- * Clears the in-session "server rejected fast mode" sticky flag. Call when the
539
- * caller is explicitly re-arming `serviceTier: "priority"` (e.g. user toggled
540
- * `/fast on` after a previous turn auto-disabled it) so the next request
541
- * actually carries `speed: "fast"` again. No-op when the map or state entry
542
- * hasn't been materialized yet.
543
- */
544
- export function clearAnthropicFastModeFallback(
545
- providerSessionState: Map<string, ProviderSessionState> | undefined,
546
- ): void {
547
- if (!providerSessionState) return;
548
- // Fast mode is re-armed session-wide (user toggled `/fast on`), so clear the
549
- // sticky flag on every per-endpoint/model Anthropic entry — plus the legacy
550
- // unscoped key — rather than a single shared object.
551
- const prefix = `${ANTHROPIC_PROVIDER_SESSION_STATE_KEY}:`;
552
- for (const [key, value] of providerSessionState) {
553
- if (key !== ANTHROPIC_PROVIDER_SESSION_STATE_KEY && !key.startsWith(prefix)) continue;
554
- (value as AnthropicProviderSessionState).fastModeDisabled = false;
555
- }
556
- }
557
- /**
558
- * Whether the direct Anthropic model's endpoint-scoped fast-mode fallback is
559
- * currently active. Reading the map directly is intentional: inspection must
560
- * not materialize a state entry for a model that has never streamed.
561
- */
562
- export function isAnthropicFastModeFallbackDisabled(
563
- providerSessionState: Map<string, ProviderSessionState> | undefined,
564
- model: Model<Api>,
565
- ): boolean {
566
- if (!providerSessionState || model.provider !== "anthropic" || model.api !== "anthropic-messages") return false;
567
- const baseUrl = resolveAnthropicBaseUrl(model as Model<"anthropic-messages">) ?? "https://api.anthropic.com";
568
- const key = anthropicProviderSessionStateKey(baseUrl, model.id);
569
- return (providerSessionState.get(key) as AnthropicProviderSessionState | undefined)?.fastModeDisabled ?? false;
570
- }
571
-
572
550
  function hasStrictAnthropicTools(params: MessageCreateParamsStreaming): boolean {
573
551
  return params.tools?.some(tool => tool.strict === true) ?? false;
574
552
  }
@@ -760,145 +738,8 @@ export function wrapFetchForCch(base: FetchImpl): FetchImpl {
760
738
  };
761
739
  }
762
740
 
763
- const CLAUDE_CLOAKING_USER_ID_REGEX =
764
- /^user_[0-9a-fA-F]{64}_account_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}_session_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
765
-
766
- export function isClaudeCloakingUserId(userId: string): boolean {
767
- return CLAUDE_CLOAKING_USER_ID_REGEX.test(userId);
768
- }
769
-
770
- /**
771
- * Real Claude Code sends `metadata.user_id` as a JSON-stringified object of the
772
- * shape `{ device_id, account_uuid, session_id, ...extra }` (see
773
- * services/api/claude.ts → getAPIMetadata). Accept that shape so callers that
774
- * supply a stable `session_id` aren't silently overwritten with fresh entropy
775
- * on every request, which would inflate the backend session count.
776
- */
777
- function isClaudeJsonUserId(userId: string): boolean {
778
- if (userId.length === 0 || userId[0] !== "{") return false;
779
- let parsed: unknown;
780
- try {
781
- parsed = JSON.parse(userId);
782
- } catch {
783
- return false;
784
- }
785
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return false;
786
- const obj = parsed as Record<string, unknown>;
787
- return typeof obj.session_id === "string" && obj.session_id.length > 0;
788
- }
789
-
790
- function extractClaudeMetadataSessionId(userId: unknown): string | undefined {
791
- if (typeof userId !== "string") return undefined;
792
- if (isClaudeCloakingUserId(userId)) {
793
- return userId.slice(userId.lastIndexOf("_session_") + "_session_".length);
794
- }
795
- if (userId.length === 0 || userId[0] !== "{") return undefined;
796
- let parsed: unknown;
797
- try {
798
- parsed = JSON.parse(userId);
799
- } catch {
800
- return undefined;
801
- }
802
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
803
- const sessionId = (parsed as Record<string, unknown>).session_id;
804
- return typeof sessionId === "string" && sessionId.length > 0 ? sessionId : undefined;
805
- }
806
-
807
- export function generateClaudeCloakingUserId(): string {
808
- const userHash = nodeCrypto.randomBytes(32).toString("hex");
809
- const accountId = nodeCrypto.randomUUID().toLowerCase();
810
- const sessionId = nodeCrypto.randomUUID().toLowerCase();
811
- return `user_${userHash}_account_${accountId}_session_${sessionId}`;
812
- }
813
-
814
- const CLAUDE_DEVICE_ID_INSTALL_HASH_DOMAIN = "omp-claude-device-id-v1:";
815
- const CLAUDE_DEVICE_ID_ACCOUNT_HASH_DOMAIN = "omp-claude-device-id-v2";
816
-
817
- export function deriveClaudeDeviceId(installId: string, accountId?: string): string {
818
- const hash = nodeCrypto.createHash("sha256");
819
- if (accountId && accountId.length > 0) {
820
- return hash
821
- .update(CLAUDE_DEVICE_ID_ACCOUNT_HASH_DOMAIN)
822
- .update("\0")
823
- .update(installId)
824
- .update("\0")
825
- .update(accountId)
826
- .digest("hex");
827
- }
828
- return hash.update(CLAUDE_DEVICE_ID_INSTALL_HASH_DOMAIN).update(installId).digest("hex");
829
- }
830
-
831
- function readMetadataString(metadata: Record<string, unknown> | undefined, key: string): string | undefined {
832
- const value = metadata?.[key];
833
- return typeof value === "string" && value.length > 0 ? value : undefined;
834
- }
835
-
836
- function readAnthropicMetadataAccountId(metadata: Record<string, unknown> | undefined): string | undefined {
837
- return (
838
- readMetadataString(metadata, "account_uuid") ??
839
- readMetadataString(metadata, "accountId") ??
840
- readMetadataString(metadata, "account_id")
841
- );
842
- }
843
-
844
- function deriveClaudeDeviceIdFromInstallId(accountId?: string): string {
845
- return deriveClaudeDeviceId(getInstallId(), accountId);
846
- }
847
-
848
- function generateClaudeJsonUserId(sessionId?: string, accountId?: string): string {
849
- const userId: Record<string, string> = {
850
- device_id: deriveClaudeDeviceIdFromInstallId(accountId),
851
- session_id: sessionId ?? nodeCrypto.randomUUID().toLowerCase(),
852
- };
853
- if (accountId && accountId.length > 0) userId.account_uuid = accountId;
854
- return JSON.stringify(userId);
855
- }
856
-
857
- /**
858
- * Resolve the `metadata.user_id` field for an Anthropic Messages request.
859
- *
860
- * For API-key tokens, an explicit caller-supplied `userId` is forwarded
861
- * verbatim and `undefined` yields no metadata. For OAuth tokens the value
862
- * must match the Claude Code attribution shape (`isClaudeCloakingUserId` or
863
- * the `{session_id, account_uuid?, device_id?}` JSON envelope) — anything
864
- * else is dropped and a fresh Claude-Code-style JSON id is generated from
865
- * `sessionId`/`accountId` so attribution stays consistent across the main
866
- * streaming path and provider-specific request builders (e.g. web search).
867
- */
868
- export function resolveAnthropicMetadataUserId(
869
- userId: unknown,
870
- isOAuthToken: boolean,
871
- sessionId?: string,
872
- accountId?: string,
873
- ): string | undefined {
874
- if (typeof userId === "string") {
875
- if (!isOAuthToken || isClaudeCloakingUserId(userId) || isClaudeJsonUserId(userId)) {
876
- return userId;
877
- }
878
- }
879
-
880
- if (!isOAuthToken) return undefined;
881
- return generateClaudeJsonUserId(sessionId, accountId);
882
- }
883
- const ANTHROPIC_BUILTIN_TOOL_NAMES = new Set(["web_search", "code_execution", "text_editor", "computer"]);
884
741
  const UMANS_WEBSEARCH_PROVIDER_HEADER = "X-Umans-Websearch-Provider";
885
742
  const UMANS_WEBSEARCH_TOOL_NAME = "web_search";
886
- export const applyClaudeToolPrefix = (name: string): string => {
887
- if (!claudeToolPrefix) return name;
888
- if (ANTHROPIC_BUILTIN_TOOL_NAMES.has(name.toLowerCase())) return name;
889
- // Always prepend (no "already prefixed" short-circuit): the prefix is a wire
890
- // transport detail applied once to internal tool names, and `stripClaudeToolPrefix`
891
- // removes exactly one prefix on receive. Skipping names that already start with the
892
- // prefix would make a tool literally named `_foo` lose its leading underscore on the
893
- // return trip (`_foo` → wire `_foo` → strip → `foo`), so the agent loop can't find it.
894
- return `${claudeToolPrefix}${name}`;
895
- };
896
-
897
- export const stripClaudeToolPrefix = (name: string): string => {
898
- if (!claudeToolPrefix) return name;
899
- if (!name.toLowerCase().startsWith(claudeToolPrefix.toLowerCase())) return name;
900
- return name.slice(claudeToolPrefix.length);
901
- };
902
743
 
903
744
  function normalizeUmansWebSearchProvider(value: string | undefined): "native" | "exa" | undefined {
904
745
  const normalized = value?.trim().toLowerCase();
@@ -1327,22 +1168,7 @@ function resolveAnthropicBaseUrl(model: Model<"anthropic-messages">, apiKey?: st
1327
1168
  if (model.provider === "github-copilot") {
1328
1169
  return normalizeAnthropicBaseUrl(resolveGitHubCopilotBaseUrl(model.baseUrl, apiKey) ?? model.baseUrl);
1329
1170
  }
1330
- if (model.provider === "anthropic" && isFoundryEnabled()) {
1331
- const foundryBaseUrl = normalizeAnthropicBaseUrl($env.FOUNDRY_BASE_URL);
1332
- if (foundryBaseUrl) {
1333
- return foundryBaseUrl;
1334
- }
1335
- }
1336
- if (model.provider === "anthropic") {
1337
- const configured = normalizeAnthropicBaseUrl(model.baseUrl);
1338
- // An explicitly configured non-official baseUrl (e.g. a models.yml provider
1339
- // override) is more specific than the generic env fallback and wins.
1340
- if (configured && !isOfficialAnthropicApiUrl(configured)) return configured;
1341
- // Otherwise ANTHROPIC_BASE_URL routes chat through an enterprise gateway
1342
- // (docs/environment-variables.md), ahead of the official default. The
1343
- // Foundry redirect is already handled above.
1344
- return normalizeAnthropicBaseUrl($env.ANTHROPIC_BASE_URL) ?? configured ?? "https://api.anthropic.com";
1345
- }
1171
+ if (model.provider === "anthropic") return resolveDirectAnthropicBaseUrl(model);
1346
1172
  return normalizeAnthropicBaseUrl(model.baseUrl);
1347
1173
  }
1348
1174
 
@@ -1791,89 +1617,6 @@ function parseAnthropicFallbackWireBlock(value: unknown): AnthropicFallbackConte
1791
1617
 
1792
1618
  const ANTHROPIC_COMPACTION_MIN_TRIGGER_TOKENS = 50_000;
1793
1619
 
1794
- /**
1795
- * Whether this model's requests reach the official Anthropic API, resolved the
1796
- * way the transport resolves it — including the Foundry and
1797
- * `ANTHROPIC_BASE_URL` reroutes that leave `compat.officialEndpoint` stale.
1798
- */
1799
- export function resolvesToOfficialAnthropicEndpoint(model: Model<"anthropic-messages">): boolean {
1800
- return isOfficialAnthropicApiUrl(resolveAnthropicBaseUrl(model));
1801
- }
1802
-
1803
- /**
1804
- * Whether server-side compaction (`compact-2026-01-12`) may be spoken for
1805
- * this model to the endpoint a request actually reaches: a model line the
1806
- * beta supports (`compat.supportsServerCompaction`, rule-owned in the
1807
- * catalog), on the official API for the first-party provider or on any
1808
- * endpoint that opted in through `remoteCompaction.enabled`, and never on one
1809
- * whose deployment contract excludes context management. The same predicate
1810
- * gates emitting the edit, attaching the beta, and replaying a persisted
1811
- * block, so a route or model change can never leave a session sending a block
1812
- * its endpoint rejects.
1813
- */
1814
- export function supportsAnthropicCompaction(model: Model<"anthropic-messages">, effectiveBaseUrl?: string): boolean {
1815
- if (!isCompactionCapableModel(model)) return false;
1816
- if (model.remoteCompaction?.enabled === true) return true;
1817
- // First-party provider is catalog policy (`first-party-provider` on the
1818
- // provider rules), never a provider-id literal. It reads its own axis
1819
- // rather than `officialEndpoint`, which stays URL-derived.
1820
- // A `transport: "pi-native"` baseUrl names the auth gateway, not the
1821
- // upstream model server: the gateway resolves the model's own provider
1822
- // server-side, so the upstream URL check cannot apply to the model's own
1823
- // transport URL — but only for the KDL-owned first-party deployment. A
1824
- // custom provider travels the same gateway to its own upstream, whose
1825
- // server-side gate stays off (unless `remoteCompaction.enabled` opts the
1826
- // route in above). An explicitly supplied foreign endpoint (e.g. a
1827
- // caller-owned client's URL) is still judged on its own merits below.
1828
- if (
1829
- model.transport === "pi-native" &&
1830
- model.compat.firstPartyProvider === true &&
1831
- (effectiveBaseUrl === undefined || effectiveBaseUrl === normalizeAnthropicBaseUrl(model.baseUrl))
1832
- ) {
1833
- return true;
1834
- }
1835
- return (
1836
- model.compat.firstPartyProvider === true &&
1837
- (effectiveBaseUrl === undefined
1838
- ? resolvesToOfficialAnthropicEndpoint(model)
1839
- : isOfficialAnthropicApiUrl(effectiveBaseUrl))
1840
- );
1841
- }
1842
-
1843
- /**
1844
- * {@link supportsAnthropicCompaction} for a request on a caller-owned client:
1845
- * the endpoint is whatever the client targets (an `AnthropicVertex` client
1846
- * carries an Anthropic model to Vertex), never the model's own routing. SDK
1847
- * clients expose it as `baseURL`; a client that exposes no endpoint only
1848
- * compacts through an explicit `remoteCompaction.enabled` opt-in.
1849
- */
1850
- export function supportsAnthropicCompactionOnClient(
1851
- model: Model<"anthropic-messages">,
1852
- client: AnthropicMessagesClientLike,
1853
- ): boolean {
1854
- const baseURL = injectedClientBaseUrl(client);
1855
- if (baseURL !== undefined) return supportsAnthropicCompaction(model, baseURL);
1856
- return isCompactionCapableModel(model) && model.remoteCompaction?.enabled === true;
1857
- }
1858
-
1859
- /**
1860
- * Effective endpoint of a caller-owned client. SDK clients expose it as
1861
- * `baseURL`; a client that exposes none leaves routing to the model.
1862
- */
1863
- function injectedClientBaseUrl(client: AnthropicMessagesClientLike): string | undefined {
1864
- const baseURL = (client as { baseURL?: unknown }).baseURL;
1865
- return typeof baseURL === "string" && baseURL.length > 0 ? baseURL : undefined;
1866
- }
1867
-
1868
- /** The model-side half of the gate: lineage support and a deployment contract that allows it. */
1869
- function isCompactionCapableModel(model: Model<"anthropic-messages">): boolean {
1870
- return (
1871
- model.compat.supportsServerCompaction === true &&
1872
- model.compat.supportsContextManagement !== false &&
1873
- model.remoteCompaction?.enabled !== false
1874
- );
1875
- }
1876
-
1877
1620
  /**
1878
1621
  * Whether a persisted compaction summary replays as a native `compaction`
1879
1622
  * block: only the provider that produced it may replay it, and only on a
@@ -4567,7 +4310,7 @@ function buildParams(
4567
4310
  // Pre-compute metadata.
4568
4311
  const metadataAccountId = readAnthropicMetadataAccountId(options?.metadata);
4569
4312
  const metadataUserId = resolveAnthropicMetadataUserId(
4570
- readMetadataString(options?.metadata, "user_id") ??
4313
+ readAnthropicMetadataString(options?.metadata, "user_id") ??
4571
4314
  // Deliberately share the normalized affinity identity across Kimi's two transports.
4572
4315
  (model.provider === "kimi-code" ? getOpenAIPromptCacheKey(options) : undefined),
4573
4316
  isOAuthToken,
@@ -1,10 +1,10 @@
1
+ import { NO_AUTH_SENTINEL } from "../auth-retry";
1
2
  import { type AwsBedrockProviderOptions, resolveAwsBearerToken } from "../registry/aws";
2
3
  import type { FetchImpl, Model } from "../types";
3
4
  import { resolveAwsRegion } from "../utils/aws-profile";
4
5
  import { invalidateAwsCredentialCache, resolveAwsCredentials } from "./aws-credentials";
5
6
  import { signRequest } from "./aws-sigv4";
6
7
  import type { OpenAIResponsesOptions } from "./openai-responses";
7
- import { NO_AUTH_SENTINEL } from "./openai-shared";
8
8
 
9
9
  export type BedrockMantleProviderOptions = AwsBedrockProviderOptions;
10
10
 
@@ -87,10 +87,6 @@ export function clearGitLabDuoDirectAccessCache(): void {
87
87
  directAccessCache.clear();
88
88
  }
89
89
 
90
- export function isGitLabDuoModel(model: Model<Api>): boolean {
91
- return model.provider === "gitlab-duo";
92
- }
93
-
94
90
  export function streamGitLabDuo(
95
91
  model: Model<Api>,
96
92
  context: Context,
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import { getKimiCommonHeaders } from "../registry/oauth/kimi";
13
- import type { Api, Context, Model } from "../types";
13
+ import type { Context, Model } from "../types";
14
14
  import type { AssistantMessageEventStream } from "../utils/event-stream";
15
15
  import {
16
16
  type OpenAIAnthropicApiFormat,
@@ -46,10 +46,3 @@ export function streamKimi(
46
46
  extraHeaders: getKimiCommonHeaders,
47
47
  });
48
48
  }
49
-
50
- /**
51
- * Check if a model is a Kimi Code model.
52
- */
53
- export function isKimiModel(model: Model<Api>): boolean {
54
- return model.provider === "kimi-code";
55
- }
@@ -0,0 +1,19 @@
1
+ /** Host integration boundary for just-in-time `x-oai-attestation` values. */
2
+ export type CodexAttestationProvider = () => Promise<string | undefined>;
3
+
4
+ let codexAttestationProvider: CodexAttestationProvider | undefined;
5
+
6
+ /** Install the process-wide just-in-time Codex attestation provider. */
7
+ export function setCodexAttestationProvider(provider: CodexAttestationProvider | undefined): void {
8
+ codexAttestationProvider = provider;
9
+ }
10
+
11
+ /** Resolve an attestation only for ChatGPT-OAuth credentials. */
12
+ export async function getCodexAttestationHeader(accountId: string | undefined): Promise<string | undefined> {
13
+ if (!accountId || !codexAttestationProvider) return undefined;
14
+ try {
15
+ return await codexAttestationProvider();
16
+ } catch {
17
+ return undefined;
18
+ }
19
+ }
@@ -0,0 +1,18 @@
1
+ import type { CodexCompactionContext, CodexCompactionRequestContext } from "../types";
2
+
3
+ /** Add the selected wire implementation to one logical compaction context. */
4
+ export function createOpenAICodexCompactionRequestContext(options: {
5
+ context: CodexCompactionContext | undefined;
6
+ implementation: "responses" | "responses_compaction_v2" | "responses_compact";
7
+ }): CodexCompactionRequestContext | undefined {
8
+ const context = options.context;
9
+ if (!context) return undefined;
10
+ return {
11
+ operationId: context.operationId,
12
+ trigger: context.trigger,
13
+ reason: context.reason,
14
+ implementation: options.implementation,
15
+ phase: context.phase,
16
+ strategy: context.strategy,
17
+ };
18
+ }