@oh-my-pi/pi-ai 18.2.5 → 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 +20 -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-broker/discover.ts +5 -3
- 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 +157 -326
- 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,54 @@
|
|
|
1
|
+
import { isOfficialAnthropicApiUrl } from "@oh-my-pi/pi-catalog/compat/anthropic";
|
|
2
|
+
import type { Model } from "../types";
|
|
3
|
+
import type { AnthropicMessagesClientLike } from "./anthropic-client";
|
|
4
|
+
import { normalizeAnthropicBaseUrl, resolveDirectAnthropicBaseUrl } from "./anthropic-state";
|
|
5
|
+
|
|
6
|
+
function isCompactionCapableModel(model: Model<"anthropic-messages">): boolean {
|
|
7
|
+
return (
|
|
8
|
+
model.compat.supportsServerCompaction === true &&
|
|
9
|
+
model.compat.supportsContextManagement !== false &&
|
|
10
|
+
model.remoteCompaction?.enabled !== false
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Whether the model's effective first-party route is the official Anthropic API. */
|
|
15
|
+
export function resolvesToOfficialAnthropicEndpoint(model: Model<"anthropic-messages">): boolean {
|
|
16
|
+
const baseUrl =
|
|
17
|
+
model.provider === "anthropic" ? resolveDirectAnthropicBaseUrl(model) : normalizeAnthropicBaseUrl(model.baseUrl);
|
|
18
|
+
return isOfficialAnthropicApiUrl(baseUrl);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Whether the model and effective endpoint support Anthropic native compaction. */
|
|
22
|
+
export function supportsAnthropicCompaction(model: Model<"anthropic-messages">, effectiveBaseUrl?: string): boolean {
|
|
23
|
+
if (!isCompactionCapableModel(model)) return false;
|
|
24
|
+
if (model.remoteCompaction?.enabled === true) return true;
|
|
25
|
+
if (
|
|
26
|
+
model.transport === "pi-native" &&
|
|
27
|
+
model.compat.firstPartyProvider === true &&
|
|
28
|
+
(effectiveBaseUrl === undefined || effectiveBaseUrl === normalizeAnthropicBaseUrl(model.baseUrl))
|
|
29
|
+
) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
return (
|
|
33
|
+
model.compat.firstPartyProvider === true &&
|
|
34
|
+
(effectiveBaseUrl === undefined
|
|
35
|
+
? resolvesToOfficialAnthropicEndpoint(model)
|
|
36
|
+
: isOfficialAnthropicApiUrl(effectiveBaseUrl))
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Read a caller-owned client's endpoint for request and compaction routing. */
|
|
41
|
+
export function injectedClientBaseUrl(client: AnthropicMessagesClientLike): string | undefined {
|
|
42
|
+
const candidate = "baseURL" in client ? client.baseURL : undefined;
|
|
43
|
+
return typeof candidate === "string" && candidate.length > 0 ? candidate : undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Whether a caller-owned Anthropic client targets a compaction-capable endpoint. */
|
|
47
|
+
export function supportsAnthropicCompactionOnClient(
|
|
48
|
+
model: Model<"anthropic-messages">,
|
|
49
|
+
client: AnthropicMessagesClientLike,
|
|
50
|
+
): boolean {
|
|
51
|
+
const baseUrl = injectedClientBaseUrl(client);
|
|
52
|
+
if (baseUrl !== undefined) return supportsAnthropicCompaction(model, baseUrl);
|
|
53
|
+
return isCompactionCapableModel(model) && model.remoteCompaction?.enabled === true;
|
|
54
|
+
}
|
|
@@ -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
|
+
}
|