@oh-my-pi/pi-ai 18.2.6 → 18.2.8
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 +29 -1
- package/THIRD-PARTY-NOTICES.txt +0 -37
- package/dist/types/auth-gateway/dispatch.d.ts +80 -0
- package/dist/types/auth-gateway/http.d.ts +5 -6
- package/dist/types/auth-gateway/index.d.ts +1 -0
- package/dist/types/auth-gateway/routes/embeddings.d.ts +3 -0
- package/dist/types/auth-gateway/routes/images.d.ts +3 -0
- package/dist/types/auth-gateway/routes/rerank.d.ts +3 -0
- package/dist/types/auth-gateway/routes/speech.d.ts +2 -0
- package/dist/types/auth-gateway/routes/systemone.d.ts +2 -0
- package/dist/types/auth-gateway/routes/transcriptions.d.ts +3 -0
- package/dist/types/auth-gateway/routes/video.d.ts +7 -0
- package/dist/types/auth-gateway/server.d.ts +10 -16
- package/dist/types/auth-retry.d.ts +2 -0
- package/dist/types/auth-storage.d.ts +2 -2
- package/dist/types/embeddings/index.d.ts +7 -0
- package/dist/types/embeddings/openai-embeddings.d.ts +15 -0
- package/dist/types/embeddings/types.d.ts +15 -0
- package/dist/types/error/classes.d.ts +4 -1
- package/dist/types/error/flags.d.ts +12 -4
- package/dist/types/images/google-antigravity.d.ts +9 -0
- package/dist/types/images/google-generative-ai.d.ts +3 -0
- package/dist/types/images/index.d.ts +14 -0
- package/dist/types/images/openai-hosted.d.ts +3 -0
- package/dist/types/images/openai-images.d.ts +5 -0
- package/dist/types/images/openrouter-images.d.ts +3 -0
- package/dist/types/images/shared.d.ts +34 -0
- package/dist/types/images/types.d.ts +31 -0
- package/dist/types/index.d.ts +21 -13
- package/dist/types/judgment/types.d.ts +5 -2
- package/dist/types/judgment/typesafe.d.ts +18 -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/embeddings-server.d.ts +32 -0
- package/dist/types/providers/gitlab-duo.d.ts +0 -1
- package/dist/types/providers/images-server.d.ts +22 -0
- 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/rerank-server.d.ts +35 -0
- package/dist/types/providers/speech-server.d.ts +8 -0
- package/dist/types/providers/synthetic.d.ts +1 -5
- package/dist/types/providers/systemone-server.d.ts +26 -0
- package/dist/types/providers/transcriptions-server.d.ts +32 -0
- package/dist/types/providers/video-server.d.ts +39 -0
- package/dist/types/rerank/index.d.ts +7 -0
- package/dist/types/rerank/openrouter-rerank.d.ts +15 -0
- package/dist/types/rerank/types.d.ts +17 -0
- package/dist/types/speech/index.d.ts +13 -0
- package/dist/types/speech/openai-speech.d.ts +3 -0
- package/dist/types/speech/transport.d.ts +7 -0
- package/dist/types/speech/types.d.ts +24 -0
- package/dist/types/speech/xai-tts.d.ts +7 -0
- package/dist/types/transcription/index.d.ts +7 -0
- package/dist/types/transcription/openai-transcriptions.d.ts +15 -0
- package/dist/types/transcription/types.d.ts +41 -0
- package/dist/types/video/index.d.ts +11 -0
- package/dist/types/video/openrouter-video.d.ts +19 -0
- package/dist/types/video/types.d.ts +62 -0
- package/package.json +30 -6
- package/src/auth-gateway/dispatch.ts +273 -0
- package/src/auth-gateway/http.ts +6 -7
- package/src/auth-gateway/index.ts +1 -0
- package/src/auth-gateway/routes/embeddings.ts +98 -0
- package/src/auth-gateway/routes/images.ts +131 -0
- package/src/auth-gateway/routes/rerank.ts +87 -0
- package/src/auth-gateway/routes/speech.ts +101 -0
- package/src/auth-gateway/routes/systemone.ts +116 -0
- package/src/auth-gateway/routes/transcriptions.ts +98 -0
- package/src/auth-gateway/routes/video.ts +243 -0
- package/src/auth-gateway/server.ts +123 -260
- package/src/auth-retry.ts +3 -0
- package/src/auth-storage.ts +28 -22
- package/src/embeddings/index.ts +17 -0
- package/src/embeddings/openai-embeddings.ts +141 -0
- package/src/embeddings/types.ts +14 -0
- package/src/error/auth-classify.ts +10 -2
- package/src/error/classes.ts +23 -3
- package/src/error/flags.ts +51 -16
- package/src/error/rate-limit.ts +1 -1
- package/src/images/google-antigravity.ts +180 -0
- package/src/images/google-generative-ai.ts +92 -0
- package/src/images/index.ts +59 -0
- package/src/images/openai-hosted.ts +185 -0
- package/src/images/openai-images.ts +110 -0
- package/src/images/openrouter-images.ts +33 -0
- package/src/images/shared.ts +193 -0
- package/src/images/types.ts +36 -0
- package/src/index.ts +22 -13
- package/src/judgment/types.ts +6 -3
- package/src/judgment/typesafe.ts +58 -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/embeddings-server.ts +151 -0
- package/src/providers/gitlab-duo.ts +0 -4
- package/src/providers/images-server.ts +159 -0
- 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/rerank-server.ts +166 -0
- package/src/providers/speech-server.ts +53 -0
- package/src/providers/synthetic.ts +1 -8
- package/src/providers/systemone-server.ts +73 -0
- package/src/providers/transcriptions-server.ts +243 -0
- package/src/providers/video-server.ts +286 -0
- package/src/registry/cloudflare-ai-gateway.ts +1 -1
- package/src/rerank/index.ts +13 -0
- package/src/rerank/openrouter-rerank.ts +136 -0
- package/src/rerank/types.ts +20 -0
- package/src/speech/index.ts +35 -0
- package/src/speech/openai-speech.ts +26 -0
- package/src/speech/transport.ts +66 -0
- package/src/speech/types.ts +37 -0
- package/src/speech/xai-tts.ts +41 -0
- package/src/stream.ts +7 -15
- package/src/transcription/index.ts +17 -0
- package/src/transcription/openai-transcriptions.ts +133 -0
- package/src/transcription/types.ts +46 -0
- package/src/utils/anthropic-auth.ts +3 -6
- package/src/video/index.ts +34 -0
- package/src/video/openrouter-video.ts +210 -0
- package/src/video/types.ts +72 -0
package/src/judgment/types.ts
CHANGED
|
@@ -115,14 +115,17 @@ export class JudgmentParseError extends Error {
|
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
/**
|
|
119
|
-
|
|
118
|
+
/**
|
|
119
|
+
* Usage from a backend that reports token counts and, optionally, one billed
|
|
120
|
+
* USD amount. Judgment pricing is input-only, so the amount lands on `input`.
|
|
121
|
+
*/
|
|
122
|
+
export function tokenUsage(input: number, output: number, cost = 0): Usage {
|
|
120
123
|
return {
|
|
121
124
|
input,
|
|
122
125
|
output,
|
|
123
126
|
cacheRead: 0,
|
|
124
127
|
cacheWrite: 0,
|
|
125
128
|
totalTokens: input + output,
|
|
126
|
-
cost: { input:
|
|
129
|
+
cost: { input: cost, output: 0, cacheRead: 0, cacheWrite: 0, total: cost },
|
|
127
130
|
};
|
|
128
131
|
}
|
package/src/judgment/typesafe.ts
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TypeSafe System One client: the native {@link Judge} backend.
|
|
3
3
|
*
|
|
4
|
-
* Forwards a {@link JudgmentRequest} verbatim to
|
|
5
|
-
* the typed answers back.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Forwards a {@link JudgmentRequest} verbatim to the judgment route of its
|
|
5
|
+
* API ({@link JUDGMENT_ROUTES}) and maps the typed answers back. TypeSafe's
|
|
6
|
+
* own `POST /v1/systemone` and OpenRouter's `POST /api/alpha/decisions` share
|
|
7
|
+
* the request and answer wire shape, so one client serves both. Credentials
|
|
8
|
+
* flow through {@link withAuth}, so a stored key rotates on 401/403 exactly
|
|
9
|
+
* like chat providers; transient 429/5xx responses retry with bounded,
|
|
10
|
+
* `retry-after`-aware backoff.
|
|
8
11
|
*
|
|
9
12
|
* Environment (mirrors the official SDK): `TYPESAFE_API_KEY` is resolved by
|
|
10
13
|
* the auth registry (`rules/auth/typesafe.kdl`), `TYPESAFE_BASE_URL`
|
|
11
14
|
* overrides the API root, `TYPESAFE_DEFAULT_MODEL` the model.
|
|
12
15
|
*/
|
|
13
|
-
import
|
|
16
|
+
import { TYPESAFE_DEFAULT_BASE_URL } from "@oh-my-pi/pi-catalog/discovery";
|
|
17
|
+
import type { Api, FetchImpl } from "@oh-my-pi/pi-catalog/types";
|
|
14
18
|
import { $env } from "@oh-my-pi/pi-utils";
|
|
15
19
|
import { type ApiKey, withAuth } from "../auth-retry";
|
|
16
20
|
import * as AIError from "../error";
|
|
@@ -26,9 +30,22 @@ import {
|
|
|
26
30
|
} from "./types";
|
|
27
31
|
|
|
28
32
|
export const TYPESAFE_PROVIDER = "typesafe";
|
|
29
|
-
export const TYPESAFE_DEFAULT_BASE_URL = "https://api.typesafe.ai";
|
|
30
33
|
export const TYPESAFE_DEFAULT_MODEL = "jev-latest";
|
|
31
34
|
|
|
35
|
+
/** Judgment `POST` path under a model's base URL, per System One–compatible API. */
|
|
36
|
+
export const JUDGMENT_ROUTES = {
|
|
37
|
+
typesafe: "/v1/systemone",
|
|
38
|
+
"openrouter-decisions": "/decisions",
|
|
39
|
+
} as const satisfies Partial<Record<Api, string>>;
|
|
40
|
+
|
|
41
|
+
/** APIs {@link TypeSafeJudge} can serve. */
|
|
42
|
+
export type JudgmentApi = keyof typeof JUDGMENT_ROUTES;
|
|
43
|
+
|
|
44
|
+
/** Whether a catalog API answers System One judgments natively. */
|
|
45
|
+
export function isJudgmentApi(api: Api): api is JudgmentApi {
|
|
46
|
+
return Object.hasOwn(JUDGMENT_ROUTES, api);
|
|
47
|
+
}
|
|
48
|
+
|
|
32
49
|
/** `TYPESAFE_BASE_URL` when set, else the public API root; trailing slashes stripped. */
|
|
33
50
|
export function typesafeBaseUrl(): string {
|
|
34
51
|
return ($env.TYPESAFE_BASE_URL?.trim() || TYPESAFE_DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
@@ -41,10 +58,16 @@ export function typesafeModel(): string {
|
|
|
41
58
|
|
|
42
59
|
export interface TypeSafeJudgeOptions {
|
|
43
60
|
apiKey: ApiKey;
|
|
61
|
+
/** Wire route; defaults to TypeSafe's own API. */
|
|
62
|
+
api?: JudgmentApi;
|
|
63
|
+
/** Catalog provider reported on results; defaults to {@link TYPESAFE_PROVIDER}. */
|
|
64
|
+
provider?: string;
|
|
44
65
|
/** Defaults to {@link typesafeBaseUrl}. */
|
|
45
66
|
baseUrl?: string;
|
|
46
67
|
/** Defaults to {@link typesafeModel}. */
|
|
47
68
|
model?: string;
|
|
69
|
+
/** Static headers attached to judgment requests (e.g. proxy routing, gateway auth). */
|
|
70
|
+
headers?: Record<string, string>;
|
|
48
71
|
fetch?: FetchImpl;
|
|
49
72
|
/** Per-attempt timeout; defaults to {@link DEFAULT_TIMEOUT_MS}. */
|
|
50
73
|
timeoutMs?: number;
|
|
@@ -60,17 +83,11 @@ const MAX_ATTEMPTS = 3;
|
|
|
60
83
|
const BACKOFF_BASE_MS = 500;
|
|
61
84
|
const BACKOFF_MAX_MS = 5_000;
|
|
62
85
|
|
|
63
|
-
/** Wire shape of `GET /v1/models`. */
|
|
64
|
-
export interface TypeSafeModelCard {
|
|
65
|
-
name: string;
|
|
66
|
-
description: string;
|
|
67
|
-
release_date: string;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
86
|
interface SystemOneResponse {
|
|
71
87
|
model: string;
|
|
72
88
|
answers: Record<string, Answer>;
|
|
73
|
-
|
|
89
|
+
/** OpenRouter adds the billed `cost` in USD; TypeSafe reports tokens only. */
|
|
90
|
+
usage: { input_tokens: number; output_tokens: number; cost?: number };
|
|
74
91
|
}
|
|
75
92
|
|
|
76
93
|
/** Server hint wins (capped); otherwise exponential backoff from {@link BACKOFF_BASE_MS}. */
|
|
@@ -82,75 +99,68 @@ function backoffMs(attempt: number, headers: Headers | undefined): number {
|
|
|
82
99
|
|
|
83
100
|
export class TypeSafeJudge implements Judge {
|
|
84
101
|
readonly label: string;
|
|
102
|
+
readonly api: JudgmentApi;
|
|
103
|
+
readonly provider: string;
|
|
85
104
|
readonly model: string;
|
|
86
105
|
readonly baseUrl: string;
|
|
87
106
|
readonly #apiKey: ApiKey;
|
|
107
|
+
readonly #headers: Record<string, string> | undefined;
|
|
88
108
|
readonly #fetch: FetchImpl;
|
|
89
109
|
readonly #timeoutMs: number;
|
|
90
110
|
|
|
91
111
|
constructor(options: TypeSafeJudgeOptions) {
|
|
92
112
|
this.#apiKey = options.apiKey;
|
|
113
|
+
this.api = options.api ?? TYPESAFE_PROVIDER;
|
|
114
|
+
this.provider = options.provider ?? TYPESAFE_PROVIDER;
|
|
93
115
|
this.baseUrl = (options.baseUrl ?? typesafeBaseUrl()).replace(/\/+$/, "");
|
|
94
116
|
this.model = options.model ?? typesafeModel();
|
|
117
|
+
this.#headers = options.headers;
|
|
95
118
|
this.#fetch = options.fetch ?? fetch;
|
|
96
119
|
this.#timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
97
|
-
this.label = `${
|
|
120
|
+
this.label = `${this.provider}/${this.model}`;
|
|
98
121
|
}
|
|
99
122
|
|
|
100
123
|
async judge<Q extends Questions>(request: JudgmentRequest<Q>, options?: JudgeOptions): Promise<JudgmentResult<Q>> {
|
|
101
124
|
const body = JSON.stringify({ state: request.state, model: this.model, questions: request.questions });
|
|
102
|
-
const
|
|
125
|
+
const signal = options?.signal;
|
|
126
|
+
const response = await withAuth(
|
|
127
|
+
this.#apiKey,
|
|
128
|
+
key => this.#attempt<SystemOneResponse>(JUDGMENT_ROUTES[this.api], body, key, signal),
|
|
129
|
+
{ signal },
|
|
130
|
+
);
|
|
103
131
|
for (const id in request.questions) {
|
|
104
132
|
const answer = response.answers[id];
|
|
105
133
|
if (answer === undefined || answer.type !== request.questions[id].type) {
|
|
106
134
|
throw new AIError.ProviderResponseError(
|
|
107
|
-
|
|
108
|
-
{ provider:
|
|
135
|
+
`${this.label} response is missing a "${request.questions[id].type}" answer for question "${id}"`,
|
|
136
|
+
{ provider: this.provider, kind: "envelope" },
|
|
109
137
|
);
|
|
110
138
|
}
|
|
111
139
|
}
|
|
112
140
|
return {
|
|
113
|
-
api:
|
|
114
|
-
provider:
|
|
141
|
+
api: this.api,
|
|
142
|
+
provider: this.provider,
|
|
115
143
|
model: response.model,
|
|
116
144
|
answers: response.answers as JudgmentResult<Q>["answers"],
|
|
117
|
-
usage: tokenUsage(response.usage.input_tokens, response.usage.output_tokens),
|
|
145
|
+
usage: tokenUsage(response.usage.input_tokens, response.usage.output_tokens, response.usage.cost),
|
|
118
146
|
};
|
|
119
147
|
}
|
|
120
148
|
|
|
121
|
-
|
|
122
|
-
async listModels(signal?: AbortSignal): Promise<TypeSafeModelCard[]> {
|
|
123
|
-
const response = await this.#request<{ models: TypeSafeModelCard[] }>("GET", "/v1/models", undefined, signal);
|
|
124
|
-
if (!Array.isArray(response.models)) {
|
|
125
|
-
throw new AIError.ProviderResponseError("TypeSafe /v1/models response is missing `models`", {
|
|
126
|
-
provider: TYPESAFE_PROVIDER,
|
|
127
|
-
kind: "envelope",
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
return response.models;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
async #request<T>(method: "GET" | "POST", path: string, body: string | undefined, signal?: AbortSignal): Promise<T> {
|
|
134
|
-
return withAuth(this.#apiKey, key => this.#attempt<T>(method, path, body, key, signal), { signal });
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
async #attempt<T>(
|
|
138
|
-
method: "GET" | "POST",
|
|
139
|
-
path: string,
|
|
140
|
-
body: string | undefined,
|
|
141
|
-
key: string,
|
|
142
|
-
signal: AbortSignal | undefined,
|
|
143
|
-
): Promise<T> {
|
|
149
|
+
async #attempt<T>(path: string, body: string, key: string, signal: AbortSignal | undefined): Promise<T> {
|
|
144
150
|
const url = `${this.baseUrl}${path}`;
|
|
145
|
-
const headers: Record<string, string> = {
|
|
146
|
-
|
|
151
|
+
const headers: Record<string, string> = {
|
|
152
|
+
...this.#headers,
|
|
153
|
+
Authorization: `Bearer ${key}`,
|
|
154
|
+
Accept: "application/json",
|
|
155
|
+
"Content-Type": "application/json",
|
|
156
|
+
};
|
|
147
157
|
for (let attempt = 0; ; attempt++) {
|
|
148
158
|
signal?.throwIfAborted();
|
|
149
159
|
const timeout = AbortSignal.timeout(this.#timeoutMs);
|
|
150
160
|
let response: Response;
|
|
151
161
|
try {
|
|
152
162
|
response = await this.#fetch(url, {
|
|
153
|
-
method,
|
|
163
|
+
method: "POST",
|
|
154
164
|
headers,
|
|
155
165
|
body,
|
|
156
166
|
signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
|
|
@@ -162,7 +172,7 @@ export class TypeSafeJudge implements Judge {
|
|
|
162
172
|
}
|
|
163
173
|
if (response.ok) return (await response.json()) as T;
|
|
164
174
|
const text = await response.text();
|
|
165
|
-
const error = new TypeSafeApiError(
|
|
175
|
+
const error = new TypeSafeApiError(`${this.label} API error (${response.status}): ${text}`, response.status, {
|
|
166
176
|
headers: response.headers,
|
|
167
177
|
});
|
|
168
178
|
const transient = response.status === 408 || response.status === 429 || response.status >= 500;
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
* very turn that rotated; it stays on its existing session-close path.
|
|
39
39
|
*/
|
|
40
40
|
|
|
41
|
-
import { clearAnthropicFastModeFallback } from "./providers/anthropic";
|
|
41
|
+
import { clearAnthropicFastModeFallback } from "./providers/anthropic-state";
|
|
42
42
|
import { resetOpenAIResponsesAccountScopedState } from "./providers/openai-responses";
|
|
43
43
|
import type { ProviderSessionState } from "./types";
|
|
44
44
|
|
|
@@ -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
|
+
}
|