@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.
- package/CHANGELOG.md +13 -1
- package/THIRD-PARTY-NOTICES.txt +0 -37
- package/dist/types/auth-retry.d.ts +2 -0
- package/dist/types/auth-storage.d.ts +2 -2
- package/dist/types/error/classes.d.ts +4 -1
- package/dist/types/error/flags.d.ts +12 -4
- package/dist/types/index.d.ts +14 -12
- package/dist/types/judgment/types.d.ts +5 -2
- package/dist/types/judgment/typesafe.d.ts +16 -22
- package/dist/types/providers/anthropic-compaction.d.ts +10 -0
- package/dist/types/providers/anthropic-identity.d.ts +18 -0
- package/dist/types/providers/anthropic-state.d.ts +13 -0
- package/dist/types/providers/anthropic.d.ts +6 -59
- package/dist/types/providers/gitlab-duo.d.ts +0 -1
- package/dist/types/providers/kimi.d.ts +1 -5
- package/dist/types/providers/openai-codex-attestation.d.ts +6 -0
- package/dist/types/providers/openai-codex-compaction.d.ts +6 -0
- package/dist/types/providers/openai-codex-responses.d.ts +3 -27
- package/dist/types/providers/openai-codex-transport.d.ts +7 -0
- package/dist/types/providers/openai-shared.d.ts +0 -9
- package/dist/types/providers/register-builtins.d.ts +20 -22
- package/dist/types/providers/synthetic.d.ts +1 -5
- package/package.json +6 -6
- package/src/auth-retry.ts +3 -0
- package/src/auth-storage.ts +28 -22
- package/src/error/auth-classify.ts +10 -2
- package/src/error/classes.ts +23 -3
- package/src/error/flags.ts +51 -16
- package/src/index.ts +15 -12
- package/src/judgment/types.ts +6 -3
- package/src/judgment/typesafe.ts +53 -48
- package/src/provider-session-state.ts +1 -1
- package/src/providers/anthropic-compaction.ts +54 -0
- package/src/providers/anthropic-identity.ts +136 -0
- package/src/providers/anthropic-state.ts +55 -0
- package/src/providers/anthropic.ts +39 -296
- package/src/providers/bedrock-mantle.ts +1 -1
- package/src/providers/gitlab-duo.ts +0 -4
- package/src/providers/kimi.ts +1 -8
- package/src/providers/openai-codex-attestation.ts +19 -0
- package/src/providers/openai-codex-compaction.ts +18 -0
- package/src/providers/openai-codex-responses.ts +9 -68
- package/src/providers/openai-codex-transport.ts +18 -0
- package/src/providers/openai-shared.ts +1 -10
- package/src/providers/register-builtins.ts +113 -320
- package/src/providers/synthetic.ts +1 -8
- package/src/registry/cloudflare-ai-gateway.ts +1 -1
- package/src/stream.ts +7 -15
- 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"
|
|
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
|
-
|
|
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,
|
package/src/providers/kimi.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { getKimiCommonHeaders } from "../registry/oauth/kimi";
|
|
13
|
-
import type {
|
|
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
|
+
}
|