@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
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { calculateCost } from "@oh-my-pi/pi-catalog/models";
|
|
2
|
+
import type { Api, FetchImpl, Model, Usage } from "@oh-my-pi/pi-catalog/types";
|
|
3
|
+
import { type } from "@oh-my-pi/omptype";
|
|
4
|
+
import { type ApiKey, withAuth } from "../auth-retry";
|
|
5
|
+
import * as AIError from "../error";
|
|
6
|
+
import type { EmbeddingRequest, EmbeddingResult } from "./types";
|
|
7
|
+
|
|
8
|
+
export interface EmbeddingOptions {
|
|
9
|
+
apiKey: ApiKey;
|
|
10
|
+
fetch?: FetchImpl;
|
|
11
|
+
signal?: AbortSignal;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Non-2xx response from an OpenAI-compatible embeddings endpoint. */
|
|
15
|
+
export class EmbeddingApiError extends AIError.ProviderHttpError {
|
|
16
|
+
override readonly name = "EmbeddingApiError";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const upstreamResponseSchema = type({
|
|
20
|
+
data: "object[]",
|
|
21
|
+
model: "string",
|
|
22
|
+
"usage?": "object",
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
interface UpstreamUsage {
|
|
26
|
+
prompt_tokens?: unknown;
|
|
27
|
+
total_tokens?: unknown;
|
|
28
|
+
cost?: unknown;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function finiteNumber(value: unknown): number | undefined {
|
|
32
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function decodeUsage(model: Model<Api>, raw: unknown): Usage {
|
|
36
|
+
const upstream = raw && typeof raw === "object" ? (raw as UpstreamUsage) : {};
|
|
37
|
+
const input = finiteNumber(upstream.prompt_tokens) ?? 0;
|
|
38
|
+
const totalTokens = finiteNumber(upstream.total_tokens) ?? input;
|
|
39
|
+
const reportedCost = finiteNumber(upstream.cost);
|
|
40
|
+
const usage: Usage = {
|
|
41
|
+
input,
|
|
42
|
+
output: 0,
|
|
43
|
+
cacheRead: 0,
|
|
44
|
+
cacheWrite: 0,
|
|
45
|
+
totalTokens,
|
|
46
|
+
...(reportedCost !== undefined && { credits: { cost: reportedCost } }),
|
|
47
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: reportedCost ?? 0 },
|
|
48
|
+
};
|
|
49
|
+
if (reportedCost === undefined) calculateCost(model, usage);
|
|
50
|
+
return usage;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function decodeEmbeddings(data: object[], model: Model<Api>): EmbeddingResult["embeddings"] {
|
|
54
|
+
return data.map((raw, position) => {
|
|
55
|
+
const index = Reflect.get(raw, "index");
|
|
56
|
+
const embedding = Reflect.get(raw, "embedding");
|
|
57
|
+
if (!Number.isInteger(index) || index < 0) {
|
|
58
|
+
throw new AIError.ProviderResponseError(
|
|
59
|
+
`${model.provider}/${model.id} embeddings response has an invalid index at data[${position}]`,
|
|
60
|
+
{ provider: model.provider, kind: "envelope" },
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
if (
|
|
64
|
+
typeof embedding !== "string" &&
|
|
65
|
+
(!Array.isArray(embedding) || !embedding.every(value => typeof value === "number" && Number.isFinite(value)))
|
|
66
|
+
) {
|
|
67
|
+
throw new AIError.ProviderResponseError(
|
|
68
|
+
`${model.provider}/${model.id} embeddings response has an invalid vector at data[${position}]`,
|
|
69
|
+
{ provider: model.provider, kind: "envelope" },
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
return { index, embedding };
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function responseError(response: Response, model: Model<Api>): Promise<EmbeddingApiError> {
|
|
77
|
+
const text = await response.text();
|
|
78
|
+
let detail = text;
|
|
79
|
+
let code: string | undefined;
|
|
80
|
+
try {
|
|
81
|
+
const parsed: unknown = JSON.parse(text);
|
|
82
|
+
if (parsed && typeof parsed === "object" && "error" in parsed) {
|
|
83
|
+
const error = parsed.error;
|
|
84
|
+
if (error && typeof error === "object") {
|
|
85
|
+
const envelope = error as { message?: unknown; code?: unknown; type?: unknown };
|
|
86
|
+
if (typeof envelope.message === "string") detail = envelope.message;
|
|
87
|
+
if (typeof envelope.code === "string") code = envelope.code;
|
|
88
|
+
else if (typeof envelope.type === "string") code = envelope.type;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
} catch {}
|
|
92
|
+
return new EmbeddingApiError(
|
|
93
|
+
`${model.provider}/${model.id} embeddings API error (${response.status}): ${detail || response.statusText}`,
|
|
94
|
+
response.status,
|
|
95
|
+
{ headers: response.headers, code },
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Call an OpenAI/OpenRouter-compatible embeddings endpoint. */
|
|
100
|
+
export async function embedOpenAI(
|
|
101
|
+
model: Model<Api>,
|
|
102
|
+
request: EmbeddingRequest,
|
|
103
|
+
options: EmbeddingOptions,
|
|
104
|
+
): Promise<EmbeddingResult> {
|
|
105
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
106
|
+
const body = {
|
|
107
|
+
model: model.id,
|
|
108
|
+
input: request.input,
|
|
109
|
+
encoding_format: request.encodingFormat,
|
|
110
|
+
...(request.dimensions !== undefined && { dimensions: request.dimensions }),
|
|
111
|
+
...(request.user !== undefined && { user: request.user }),
|
|
112
|
+
};
|
|
113
|
+
const response = await withAuth(
|
|
114
|
+
options.apiKey,
|
|
115
|
+
async key => {
|
|
116
|
+
const attempt = await fetchImpl(`${model.baseUrl.replace(/\/+$/, "")}/embeddings`, {
|
|
117
|
+
method: "POST",
|
|
118
|
+
headers: { Authorization: `Bearer ${key}`, Accept: "application/json", "Content-Type": "application/json" },
|
|
119
|
+
body: JSON.stringify(body),
|
|
120
|
+
signal: options.signal,
|
|
121
|
+
});
|
|
122
|
+
if (!attempt.ok) throw await responseError(attempt, model);
|
|
123
|
+
return attempt;
|
|
124
|
+
},
|
|
125
|
+
{ signal: options.signal },
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const raw: unknown = await response.json();
|
|
129
|
+
const parsed = upstreamResponseSchema(raw);
|
|
130
|
+
if (parsed instanceof type.errors) {
|
|
131
|
+
throw new AIError.ProviderResponseError(
|
|
132
|
+
`${model.provider}/${model.id} embeddings response is malformed: ${parsed.summary}`,
|
|
133
|
+
{ provider: model.provider, kind: "envelope" },
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
embeddings: decodeEmbeddings(parsed.data, model),
|
|
138
|
+
model: parsed.model,
|
|
139
|
+
usage: decodeUsage(model, parsed.usage),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Usage } from "@oh-my-pi/pi-catalog/types";
|
|
2
|
+
|
|
3
|
+
export interface EmbeddingRequest {
|
|
4
|
+
input: string | string[] | number[] | number[][];
|
|
5
|
+
dimensions?: number;
|
|
6
|
+
encodingFormat: "float" | "base64";
|
|
7
|
+
user?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface EmbeddingResult {
|
|
11
|
+
embeddings: Array<{ index: number; embedding: number[] | string }>;
|
|
12
|
+
model: string;
|
|
13
|
+
usage: Usage;
|
|
14
|
+
}
|
|
@@ -41,8 +41,16 @@ export function isAuthRetryableError(error: unknown): boolean {
|
|
|
41
41
|
if (isUsageLimit(error)) return true;
|
|
42
42
|
if (isAccountPolicyError(error)) return true;
|
|
43
43
|
if (isInvalidatedOAuthTokenError(error)) return true;
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
let httpStatus = extractHttpStatusFromError(error);
|
|
45
|
+
let message = error instanceof Error ? error.message : typeof error === "string" ? error : undefined;
|
|
46
|
+
if (typeof error === "object" && error !== null) {
|
|
47
|
+
if (httpStatus === undefined && "errorStatus" in error && typeof error.errorStatus === "number") {
|
|
48
|
+
httpStatus = error.errorStatus;
|
|
49
|
+
}
|
|
50
|
+
if (message === undefined && "errorMessage" in error && typeof error.errorMessage === "string") {
|
|
51
|
+
message = error.errorMessage;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
46
54
|
const embeddedStatus = message ? extractHttpStatusFromError({ message }) : undefined;
|
|
47
55
|
const status = httpStatus ?? embeddedStatus;
|
|
48
56
|
if (isConcurrencyCapExclusion(status, message)) return false;
|
package/src/error/classes.ts
CHANGED
|
@@ -90,8 +90,8 @@ export class AnthropicApiError extends ProviderHttpError {
|
|
|
90
90
|
declare readonly headers: Headers;
|
|
91
91
|
readonly requestId: string | null;
|
|
92
92
|
|
|
93
|
-
constructor(status: number, message: string, headers: Headers) {
|
|
94
|
-
super(message, status, { headers });
|
|
93
|
+
constructor(status: number, message: string, headers: Headers, options?: { code?: string; cause?: unknown }) {
|
|
94
|
+
super(message, status, { headers, code: options?.code, cause: options?.cause });
|
|
95
95
|
this.name = "AnthropicApiError";
|
|
96
96
|
this.requestId = headers.get("request-id");
|
|
97
97
|
}
|
|
@@ -176,7 +176,27 @@ export class AnthropicApiError extends ProviderHttpError {
|
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
const detail = bodyChunks.join("").trim() || "status code (no body)";
|
|
179
|
-
|
|
179
|
+
let code: string | undefined;
|
|
180
|
+
try {
|
|
181
|
+
const parsed: unknown = JSON.parse(detail);
|
|
182
|
+
if (parsed && typeof parsed === "object" && "error" in parsed) {
|
|
183
|
+
const err = parsed.error;
|
|
184
|
+
if (err && typeof err === "object") {
|
|
185
|
+
if (
|
|
186
|
+
"details" in err &&
|
|
187
|
+
err.details &&
|
|
188
|
+
typeof err.details === "object" &&
|
|
189
|
+
"error_code" in err.details &&
|
|
190
|
+
typeof err.details.error_code === "string"
|
|
191
|
+
) {
|
|
192
|
+
code = err.details.error_code;
|
|
193
|
+
} else if ("type" in err && typeof err.type === "string") {
|
|
194
|
+
code = err.type;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
} catch {}
|
|
199
|
+
return new AnthropicApiError(response.status, `${response.status} ${detail}`, response.headers, { code });
|
|
180
200
|
}
|
|
181
201
|
}
|
|
182
202
|
|
package/src/error/flags.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isUnexpectedSocketCloseMessage } from "@oh-my-pi/pi-utils/fetch-retry";
|
|
2
|
-
import type { Api, AssistantMessage } from "../types";
|
|
2
|
+
import type { Api, AssistantMessage, Usage } from "../types";
|
|
3
3
|
import { AwsCredentialsError } from "./aws";
|
|
4
4
|
import {
|
|
5
5
|
AnthropicConnectionError,
|
|
@@ -194,6 +194,17 @@ const PROVIDER_FINISH_ERROR_PATTERN = /\bProvider (?:returned error finish_reaso
|
|
|
194
194
|
const EMPTY_RESPONSE_PATTERN = /\bthought-only response without final output\b/i;
|
|
195
195
|
const CONTENT_FILTER_PATTERN = /\b(?:incomplete:\s*)?content_filter\b/i;
|
|
196
196
|
const ACCOUNT_POLICY_PATTERN = /\bcyber_policy\b|trusted access for cyber/i;
|
|
197
|
+
export const ANTHROPIC_ACCOUNT_POLICY_PATTERN =
|
|
198
|
+
/\b(?:oauth_not_allowed_for_organization|permission_error)\b|\bOAuth authentication is currently not allowed for this organization\b/i;
|
|
199
|
+
|
|
200
|
+
/** Whether an error message represents an Anthropic account-scoped permission/policy denial. */
|
|
201
|
+
export function isAnthropicAccountPolicyText(text: string, provider?: string, statusArg?: number): boolean {
|
|
202
|
+
if (provider !== undefined && provider !== "anthropic") return false;
|
|
203
|
+
const statusCandidate = statusArg ?? (text ? status({ message: text }) : undefined);
|
|
204
|
+
if (statusCandidate !== undefined && statusCandidate !== 403) return false;
|
|
205
|
+
return ANTHROPIC_ACCOUNT_POLICY_PATTERN.test(text);
|
|
206
|
+
}
|
|
207
|
+
|
|
197
208
|
const CODEX_CHATGPT_ACCOUNT_MODEL_POLICY_PATTERN =
|
|
198
209
|
/\bThe ['"]([^'"\r\n]+)['"] model is not supported when using Codex with a ChatGPT account\./i;
|
|
199
210
|
const CODEX_CHATGPT_ACCOUNT_MODEL_MAX_LENGTH = 256;
|
|
@@ -381,6 +392,9 @@ function statusInternal(error: unknown, depth: number): number | undefined {
|
|
|
381
392
|
if (typeof errObj.statusCode === "number" && errObj.statusCode >= 100 && errObj.statusCode <= 599) {
|
|
382
393
|
return errObj.statusCode;
|
|
383
394
|
}
|
|
395
|
+
if (typeof errObj.errorStatus === "number" && errObj.errorStatus >= 100 && errObj.errorStatus <= 599) {
|
|
396
|
+
return errObj.errorStatus;
|
|
397
|
+
}
|
|
384
398
|
if (typeof errObj.response === "object" && errObj.response !== null) {
|
|
385
399
|
const resp = errObj.response as Record<string, unknown>;
|
|
386
400
|
if (typeof resp.status === "number" && resp.status >= 100 && resp.status <= 599) {
|
|
@@ -490,6 +504,8 @@ function classifyText(
|
|
|
490
504
|
if (isProviderFinishErrorText(errorMessage)) kinds |= Flag.ProviderFinishError;
|
|
491
505
|
if (EMPTY_RESPONSE_PATTERN.test(errorMessage)) kinds |= Flag.EmptyResponse | Flag.Transient;
|
|
492
506
|
if (isContentBlockedText(errorMessage)) kinds |= Flag.ContentBlocked;
|
|
507
|
+
const statusClean = errorStatus ? errorStatus : (status({ message: errorMessage }) ?? undefined);
|
|
508
|
+
|
|
493
509
|
if (
|
|
494
510
|
ACCOUNT_POLICY_PATTERN.test(errorMessage) ||
|
|
495
511
|
isCodexChatGPTAccountPolicyText(errorMessage, provider, modelId) ||
|
|
@@ -497,9 +513,10 @@ function classifyText(
|
|
|
497
513
|
) {
|
|
498
514
|
kinds |= Flag.AccountPolicy | Flag.ContentBlocked;
|
|
499
515
|
}
|
|
516
|
+
if (isAnthropicAccountPolicyText(errorMessage, provider, statusClean)) {
|
|
517
|
+
kinds |= Flag.AccountPolicy;
|
|
518
|
+
}
|
|
500
519
|
if (isAuthFailureText(errorMessage)) kinds |= Flag.AuthFailed;
|
|
501
|
-
|
|
502
|
-
const statusClean = errorStatus ? errorStatus : (status({ message: errorMessage }) ?? undefined);
|
|
503
520
|
const cleanMessage = errorMessage;
|
|
504
521
|
const isOpaque = isOpaqueStatusBody(cleanMessage);
|
|
505
522
|
|
|
@@ -597,8 +614,12 @@ export function classify(error: unknown, api?: Api): number {
|
|
|
597
614
|
if ("errorId" in link && typeof (link as { errorId: unknown }).errorId === "number") {
|
|
598
615
|
kinds |= (link as { errorId: number }).errorId & KIND_MASK;
|
|
599
616
|
}
|
|
600
|
-
if ("code" in link && typeof link.code === "string"
|
|
601
|
-
|
|
617
|
+
if ("code" in link && typeof link.code === "string") {
|
|
618
|
+
if (ACCOUNT_POLICY_PATTERN.test(link.code)) {
|
|
619
|
+
kinds |= Flag.AccountPolicy | Flag.ContentBlocked;
|
|
620
|
+
} else if (ANTHROPIC_ACCOUNT_POLICY_PATTERN.test(link.code)) {
|
|
621
|
+
kinds |= Flag.AccountPolicy;
|
|
622
|
+
}
|
|
602
623
|
}
|
|
603
624
|
}
|
|
604
625
|
|
|
@@ -635,6 +656,13 @@ export function classify(error: unknown, api?: Api): number {
|
|
|
635
656
|
if (code === "overloaded_error" || code === "rate_limit_error") {
|
|
636
657
|
linkKinds |= Flag.Transient;
|
|
637
658
|
}
|
|
659
|
+
if (
|
|
660
|
+
code === "oauth_not_allowed_for_organization" ||
|
|
661
|
+
code === "permission_error" ||
|
|
662
|
+
(codeStatus === 403 && ANTHROPIC_ACCOUNT_POLICY_PATTERN.test(link.message))
|
|
663
|
+
) {
|
|
664
|
+
linkKinds |= Flag.AccountPolicy;
|
|
665
|
+
}
|
|
638
666
|
if (
|
|
639
667
|
(codeStatus === 401 || codeStatus === 403) &&
|
|
640
668
|
!(codeStatus === 403 && parseRateLimitReason(link.message) === "CONCURRENT_LIMIT")
|
|
@@ -655,12 +683,12 @@ export function classify(error: unknown, api?: Api): number {
|
|
|
655
683
|
linkMessage = link.message;
|
|
656
684
|
} else if (typeof link === "string") {
|
|
657
685
|
linkMessage = link;
|
|
658
|
-
} else if (
|
|
659
|
-
typeof link === "
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
686
|
+
} else if (typeof link === "object") {
|
|
687
|
+
if ("message" in link && typeof link.message === "string") {
|
|
688
|
+
linkMessage = link.message;
|
|
689
|
+
} else if ("errorMessage" in link && typeof link.errorMessage === "string") {
|
|
690
|
+
linkMessage = link.errorMessage;
|
|
691
|
+
}
|
|
664
692
|
}
|
|
665
693
|
|
|
666
694
|
const linkStatus = status(link);
|
|
@@ -834,14 +862,21 @@ export function attach<E extends object>(error: E, id: number): E {
|
|
|
834
862
|
return error;
|
|
835
863
|
}
|
|
836
864
|
|
|
865
|
+
/** Overflow-classification evidence, including errors received before token usage is available. */
|
|
866
|
+
export interface ContextOverflowMessage extends Pick<AssistantMessage, "errorId" | "stopReason" | "errorMessage"> {
|
|
867
|
+
readonly usage?: Pick<Usage, "input" | "cacheRead" | "cacheWrite">;
|
|
868
|
+
}
|
|
869
|
+
|
|
837
870
|
/** Provider-reported usage proves context-window excess — authoritative, compaction-owned (#9235). */
|
|
838
|
-
export function isUsageBackedContextOverflow(message:
|
|
839
|
-
|
|
840
|
-
|
|
871
|
+
export function isUsageBackedContextOverflow(message: ContextOverflowMessage, contextWindow?: number): boolean {
|
|
872
|
+
const usage = message.usage;
|
|
873
|
+
if (!contextWindow || !usage) return false;
|
|
874
|
+
const inputTokens = usage.input + usage.cacheRead + usage.cacheWrite;
|
|
841
875
|
return inputTokens > contextWindow;
|
|
842
876
|
}
|
|
843
877
|
|
|
844
|
-
|
|
878
|
+
/** Classify overflow from error flags, available token usage, or provider error text. */
|
|
879
|
+
export function isContextOverflow(message: ContextOverflowMessage, contextWindow?: number): boolean {
|
|
845
880
|
if (is(message.errorId, Flag.ContextOverflow)) return true;
|
|
846
881
|
if (isUsageBackedContextOverflow(message, contextWindow)) return true;
|
|
847
882
|
return message.stopReason === "error" && !!message.errorMessage && matchesOverflowText(message.errorMessage);
|
|
@@ -861,7 +896,7 @@ export function isPayloadRejection(message: AssistantMessage): boolean {
|
|
|
861
896
|
* Usage-backed overflows are authoritative window excesses and never ambiguous. */
|
|
862
897
|
export function isTextAmbiguousContextOverflow(
|
|
863
898
|
errorId: number,
|
|
864
|
-
message:
|
|
899
|
+
message: ContextOverflowMessage | undefined,
|
|
865
900
|
contextWindow?: number,
|
|
866
901
|
): boolean {
|
|
867
902
|
const overflowFlagged =
|
package/src/error/rate-limit.ts
CHANGED
|
@@ -306,7 +306,7 @@ export function calculateRateLimitBackoffMs(reason: RateLimitReason): number {
|
|
|
306
306
|
|
|
307
307
|
/** Detect usage/quota limit errors in error messages (persistent, requires credential switch). */
|
|
308
308
|
const USAGE_LIMIT_PATTERN =
|
|
309
|
-
/usage.?limit|usage_limit_reached|usage_not_included|limit_reached|quota.?(?:exceeded|reached|insufficient)|额度不足|额度耗尽|resource.?exhausted|exhausted your capacity|quota will reset|insufficient.?(?:balance|quota)|balance.?exhausted|run out of credits|out of credits|spending[- _]?limit|personal-team-blocked|clinepass limit|free limit reached on model|access_terminated_error/i;
|
|
309
|
+
/usage.?limit|usage_limit_reached|usage_not_included|limit_reached|quota.?(?:exceeded|reached|insufficient)|额度不足|额度耗尽|resource.?exhausted|exhausted your capacity|quota will reset|insufficient.?(?:balance|quota)|balance.?exhausted|run out of credits|out of credits|out of (?:extra )?usage|spending[- _]?limit|personal-team-blocked|clinepass limit|free limit reached on model|access_terminated_error/i;
|
|
310
310
|
|
|
311
311
|
/**
|
|
312
312
|
* HTTP status codes that, absent richer body classification, represent an
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ANTIGRAVITY_PRIMARY_ENDPOINT,
|
|
3
|
+
ANTIGRAVITY_SANDBOX_ENDPOINT,
|
|
4
|
+
fetchAntigravityImageModel,
|
|
5
|
+
} from "@oh-my-pi/pi-catalog/discovery/antigravity";
|
|
6
|
+
import type { Model } from "@oh-my-pi/pi-catalog/types";
|
|
7
|
+
import { getAntigravityUserAgent } from "@oh-my-pi/pi-catalog/wire/gemini-headers";
|
|
8
|
+
import { readSseJson } from "@oh-my-pi/pi-utils";
|
|
9
|
+
import { withAuth } from "../auth-retry";
|
|
10
|
+
import * as AIError from "../error";
|
|
11
|
+
import { errorMessage, ImageApiError, usageFromWire } from "./shared";
|
|
12
|
+
import type { GeneratedImage, ImageGenerationOptions, ImageGenerationRequest, ImageGenerationResult } from "./types";
|
|
13
|
+
|
|
14
|
+
const IMAGE_SYSTEM_INSTRUCTION =
|
|
15
|
+
"You are an AI image generator. Generate images based on user descriptions. Focus on creating high-quality, visually appealing images that match the user's request.";
|
|
16
|
+
|
|
17
|
+
interface AntigravityCredentials {
|
|
18
|
+
accessToken: string;
|
|
19
|
+
projectId: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface AntigravityTarget {
|
|
23
|
+
model: string;
|
|
24
|
+
endpoints: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface AntigravityChunk {
|
|
28
|
+
response?: {
|
|
29
|
+
candidates?: Array<{
|
|
30
|
+
content?: { parts?: Array<{ text?: string; inlineData?: { data?: string; mimeType?: string } }> };
|
|
31
|
+
}>;
|
|
32
|
+
usageMetadata?: { promptTokenCount?: number; candidatesTokenCount?: number };
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function parseAntigravityCredentials(raw: string): AntigravityCredentials | undefined {
|
|
37
|
+
try {
|
|
38
|
+
const parsed = JSON.parse(raw) as { token?: unknown; projectId?: unknown };
|
|
39
|
+
if (typeof parsed.token === "string" && typeof parsed.projectId === "string") {
|
|
40
|
+
return { accessToken: parsed.token, projectId: parsed.projectId };
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
// Report the same validation error below.
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function antigravityEndpoints(model: Model): string[] {
|
|
49
|
+
const configured = model.baseUrl.replace(/\/+$/, "");
|
|
50
|
+
return [...new Set([configured, ANTIGRAVITY_PRIMARY_ENDPOINT, ANTIGRAVITY_SANDBOX_ENDPOINT])];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function resolveTarget(
|
|
54
|
+
model: Model,
|
|
55
|
+
credentials: AntigravityCredentials,
|
|
56
|
+
fetchImpl: ImageGenerationOptions["fetch"],
|
|
57
|
+
signal?: AbortSignal,
|
|
58
|
+
): Promise<AntigravityTarget> {
|
|
59
|
+
const endpoints = antigravityEndpoints(model);
|
|
60
|
+
const advertised = await fetchAntigravityImageModel({
|
|
61
|
+
token: credentials.accessToken,
|
|
62
|
+
endpoint: endpoints.length === 1 ? endpoints[0] : undefined,
|
|
63
|
+
userAgent: getAntigravityUserAgent(),
|
|
64
|
+
signal,
|
|
65
|
+
fetcher: fetchImpl,
|
|
66
|
+
});
|
|
67
|
+
return advertised
|
|
68
|
+
? {
|
|
69
|
+
model: advertised.id,
|
|
70
|
+
endpoints: [advertised.endpoint, ...endpoints.filter(endpoint => endpoint !== advertised.endpoint)],
|
|
71
|
+
}
|
|
72
|
+
: { model: model.requestModelId ?? model.id, endpoints };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function buildRequest(request: ImageGenerationRequest, model: string, projectId: string): Record<string, unknown> {
|
|
76
|
+
const parts: Array<{ text?: string; inlineData?: GeneratedImage }> = (request.inputImages ?? []).map(image => ({
|
|
77
|
+
inlineData: image,
|
|
78
|
+
}));
|
|
79
|
+
parts.push({ text: request.prompt });
|
|
80
|
+
const imageConfig =
|
|
81
|
+
request.aspectRatio || request.imageSize
|
|
82
|
+
? { aspectRatio: request.aspectRatio, imageSize: request.imageSize }
|
|
83
|
+
: undefined;
|
|
84
|
+
return {
|
|
85
|
+
project: projectId,
|
|
86
|
+
model,
|
|
87
|
+
request: {
|
|
88
|
+
contents: [{ role: "user", parts }],
|
|
89
|
+
systemInstruction: { parts: [{ text: IMAGE_SYSTEM_INSTRUCTION }] },
|
|
90
|
+
generationConfig: {
|
|
91
|
+
responseModalities: ["IMAGE"],
|
|
92
|
+
...(imageConfig ? { imageConfig } : {}),
|
|
93
|
+
candidateCount: request.count ?? 1,
|
|
94
|
+
},
|
|
95
|
+
safetySettings: [
|
|
96
|
+
{ category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_ONLY_HIGH" },
|
|
97
|
+
{ category: "HARM_CATEGORY_HATE_SPEECH", threshold: "BLOCK_ONLY_HIGH" },
|
|
98
|
+
{ category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", threshold: "BLOCK_ONLY_HIGH" },
|
|
99
|
+
{ category: "HARM_CATEGORY_DANGEROUS_CONTENT", threshold: "BLOCK_ONLY_HIGH" },
|
|
100
|
+
{ category: "HARM_CATEGORY_CIVIC_INTEGRITY", threshold: "BLOCK_ONLY_HIGH" },
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
requestType: "agent",
|
|
104
|
+
requestId: `agent-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,
|
|
105
|
+
userAgent: "antigravity",
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function parseSse(response: Response, signal?: AbortSignal): Promise<ImageGenerationResult> {
|
|
110
|
+
if (!response.body) {
|
|
111
|
+
throw new AIError.ProviderResponseError("Antigravity image response has no body", { kind: "empty-body" });
|
|
112
|
+
}
|
|
113
|
+
const images: GeneratedImage[] = [];
|
|
114
|
+
const texts: string[] = [];
|
|
115
|
+
let usage = usageFromWire(undefined);
|
|
116
|
+
for await (const chunk of readSseJson<AntigravityChunk>(response.body, signal)) {
|
|
117
|
+
for (const candidate of chunk.response?.candidates ?? []) {
|
|
118
|
+
for (const part of candidate.content?.parts ?? []) {
|
|
119
|
+
if (part.text) texts.push(part.text);
|
|
120
|
+
if (part.inlineData?.data && part.inlineData.mimeType) {
|
|
121
|
+
images.push({ data: part.inlineData.data, mimeType: part.inlineData.mimeType });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const metadata = chunk.response?.usageMetadata;
|
|
126
|
+
if (metadata) {
|
|
127
|
+
usage = usageFromWire({
|
|
128
|
+
input_tokens: metadata.promptTokenCount,
|
|
129
|
+
output_tokens: metadata.candidatesTokenCount,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const text = texts.join(" ").trim();
|
|
134
|
+
return { images, ...(text ? { text } : {}), usage };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function generateAntigravityImage(
|
|
138
|
+
model: Model,
|
|
139
|
+
request: ImageGenerationRequest,
|
|
140
|
+
options: ImageGenerationOptions,
|
|
141
|
+
): Promise<ImageGenerationResult> {
|
|
142
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
143
|
+
const response = await withAuth(
|
|
144
|
+
options.apiKey,
|
|
145
|
+
async rawKey => {
|
|
146
|
+
const credentials = parseAntigravityCredentials(rawKey);
|
|
147
|
+
if (!credentials) {
|
|
148
|
+
throw new AIError.ValidationError("Antigravity image credentials must contain token and projectId");
|
|
149
|
+
}
|
|
150
|
+
const target = await resolveTarget(model, credentials, fetchImpl, options.signal);
|
|
151
|
+
const body = buildRequest(request, target.model, credentials.projectId);
|
|
152
|
+
let lastError: ImageApiError | undefined;
|
|
153
|
+
for (let index = 0; index < target.endpoints.length; index++) {
|
|
154
|
+
const result = await fetchImpl(`${target.endpoints[index]}/v1internal:streamGenerateContent?alt=sse`, {
|
|
155
|
+
method: "POST",
|
|
156
|
+
headers: {
|
|
157
|
+
Authorization: `Bearer ${credentials.accessToken}`,
|
|
158
|
+
"Content-Type": "application/json",
|
|
159
|
+
Accept: "text/event-stream",
|
|
160
|
+
"User-Agent": getAntigravityUserAgent(),
|
|
161
|
+
},
|
|
162
|
+
body: JSON.stringify(body),
|
|
163
|
+
signal: options.signal,
|
|
164
|
+
});
|
|
165
|
+
if (result.ok) return result;
|
|
166
|
+
const text = await result.text();
|
|
167
|
+
lastError = new ImageApiError(
|
|
168
|
+
`${model.provider}/${model.id} image request failed (${result.status}): ${errorMessage(text)}`,
|
|
169
|
+
result.status,
|
|
170
|
+
{ headers: result.headers },
|
|
171
|
+
);
|
|
172
|
+
const retryable = result.status === 429 || result.status >= 500;
|
|
173
|
+
if (!retryable || index === target.endpoints.length - 1) throw lastError;
|
|
174
|
+
}
|
|
175
|
+
throw lastError ?? new AIError.ProviderResponseError("Antigravity image request failed");
|
|
176
|
+
},
|
|
177
|
+
{ signal: options.signal },
|
|
178
|
+
);
|
|
179
|
+
return parseSse(response, options.signal);
|
|
180
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { Model } from "@oh-my-pi/pi-catalog/types";
|
|
2
|
+
import { withAuth } from "../auth-retry";
|
|
3
|
+
import * as AIError from "../error";
|
|
4
|
+
import { errorMessage, ImageApiError, imageBaseUrl, modelHeaders, usageFromWire } from "./shared";
|
|
5
|
+
import type { GeneratedImage, ImageGenerationOptions, ImageGenerationRequest, ImageGenerationResult } from "./types";
|
|
6
|
+
|
|
7
|
+
interface GeminiPart {
|
|
8
|
+
text?: string;
|
|
9
|
+
inlineData?: { data?: string; mimeType?: string };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface GeminiResponse {
|
|
13
|
+
candidates?: Array<{ content?: { parts?: GeminiPart[] } }>;
|
|
14
|
+
usageMetadata?: { promptTokenCount?: number; candidatesTokenCount?: number; totalTokenCount?: number };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function generateGoogleImage(
|
|
18
|
+
model: Model,
|
|
19
|
+
request: ImageGenerationRequest,
|
|
20
|
+
options: ImageGenerationOptions,
|
|
21
|
+
): Promise<ImageGenerationResult> {
|
|
22
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
23
|
+
const parts: Array<{ text?: string; inlineData?: GeneratedImage }> = (request.inputImages ?? []).map(image => ({
|
|
24
|
+
inlineData: image,
|
|
25
|
+
}));
|
|
26
|
+
parts.push({ text: request.prompt });
|
|
27
|
+
const imageConfig =
|
|
28
|
+
request.aspectRatio || request.imageSize
|
|
29
|
+
? { aspectRatio: request.aspectRatio, imageSize: request.imageSize }
|
|
30
|
+
: undefined;
|
|
31
|
+
const body = {
|
|
32
|
+
contents: [{ role: "user", parts }],
|
|
33
|
+
generationConfig: {
|
|
34
|
+
responseModalities: ["IMAGE"],
|
|
35
|
+
...(imageConfig ? { imageConfig } : {}),
|
|
36
|
+
...(request.count ? { candidateCount: request.count } : {}),
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
const response = await withAuth(
|
|
40
|
+
options.apiKey,
|
|
41
|
+
async key => {
|
|
42
|
+
const result = await fetchImpl(
|
|
43
|
+
`${imageBaseUrl(model)}/models/${encodeURIComponent(model.requestModelId ?? model.id)}:generateContent`,
|
|
44
|
+
{
|
|
45
|
+
method: "POST",
|
|
46
|
+
headers: {
|
|
47
|
+
...(await modelHeaders(model, options.signal)),
|
|
48
|
+
"Content-Type": "application/json",
|
|
49
|
+
"x-goog-api-key": key,
|
|
50
|
+
},
|
|
51
|
+
body: JSON.stringify(body),
|
|
52
|
+
signal: options.signal,
|
|
53
|
+
},
|
|
54
|
+
);
|
|
55
|
+
const text = await result.text();
|
|
56
|
+
if (!result.ok) {
|
|
57
|
+
throw new ImageApiError(
|
|
58
|
+
`${model.provider}/${model.id} image request failed (${result.status}): ${errorMessage(text)}`,
|
|
59
|
+
result.status,
|
|
60
|
+
{ headers: result.headers },
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
return JSON.parse(text) as GeminiResponse;
|
|
65
|
+
} catch (cause) {
|
|
66
|
+
throw new AIError.ProviderResponseError("Gemini image API returned malformed JSON", {
|
|
67
|
+
provider: model.provider,
|
|
68
|
+
kind: "envelope",
|
|
69
|
+
cause,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
{ signal: options.signal },
|
|
74
|
+
);
|
|
75
|
+
const responseParts = response.candidates?.flatMap(candidate => candidate.content?.parts ?? []) ?? [];
|
|
76
|
+
const images: GeneratedImage[] = [];
|
|
77
|
+
const textParts: string[] = [];
|
|
78
|
+
for (const part of responseParts) {
|
|
79
|
+
if (part.text) textParts.push(part.text);
|
|
80
|
+
if (part.inlineData?.data && part.inlineData.mimeType) {
|
|
81
|
+
images.push({ data: part.inlineData.data, mimeType: part.inlineData.mimeType });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const text = textParts.join("\n").trim();
|
|
85
|
+
const wireUsage = response.usageMetadata;
|
|
86
|
+
const usage = usageFromWire(
|
|
87
|
+
wireUsage
|
|
88
|
+
? { input_tokens: wireUsage.promptTokenCount, output_tokens: wireUsage.candidatesTokenCount }
|
|
89
|
+
: undefined,
|
|
90
|
+
);
|
|
91
|
+
return { images, ...(text ? { text } : {}), usage };
|
|
92
|
+
}
|