@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.
Files changed (136) hide show
  1. package/CHANGELOG.md +29 -1
  2. package/THIRD-PARTY-NOTICES.txt +0 -37
  3. package/dist/types/auth-gateway/dispatch.d.ts +80 -0
  4. package/dist/types/auth-gateway/http.d.ts +5 -6
  5. package/dist/types/auth-gateway/index.d.ts +1 -0
  6. package/dist/types/auth-gateway/routes/embeddings.d.ts +3 -0
  7. package/dist/types/auth-gateway/routes/images.d.ts +3 -0
  8. package/dist/types/auth-gateway/routes/rerank.d.ts +3 -0
  9. package/dist/types/auth-gateway/routes/speech.d.ts +2 -0
  10. package/dist/types/auth-gateway/routes/systemone.d.ts +2 -0
  11. package/dist/types/auth-gateway/routes/transcriptions.d.ts +3 -0
  12. package/dist/types/auth-gateway/routes/video.d.ts +7 -0
  13. package/dist/types/auth-gateway/server.d.ts +10 -16
  14. package/dist/types/auth-retry.d.ts +2 -0
  15. package/dist/types/auth-storage.d.ts +2 -2
  16. package/dist/types/embeddings/index.d.ts +7 -0
  17. package/dist/types/embeddings/openai-embeddings.d.ts +15 -0
  18. package/dist/types/embeddings/types.d.ts +15 -0
  19. package/dist/types/error/classes.d.ts +4 -1
  20. package/dist/types/error/flags.d.ts +12 -4
  21. package/dist/types/images/google-antigravity.d.ts +9 -0
  22. package/dist/types/images/google-generative-ai.d.ts +3 -0
  23. package/dist/types/images/index.d.ts +14 -0
  24. package/dist/types/images/openai-hosted.d.ts +3 -0
  25. package/dist/types/images/openai-images.d.ts +5 -0
  26. package/dist/types/images/openrouter-images.d.ts +3 -0
  27. package/dist/types/images/shared.d.ts +34 -0
  28. package/dist/types/images/types.d.ts +31 -0
  29. package/dist/types/index.d.ts +21 -13
  30. package/dist/types/judgment/types.d.ts +5 -2
  31. package/dist/types/judgment/typesafe.d.ts +18 -22
  32. package/dist/types/providers/anthropic-compaction.d.ts +10 -0
  33. package/dist/types/providers/anthropic-identity.d.ts +18 -0
  34. package/dist/types/providers/anthropic-state.d.ts +13 -0
  35. package/dist/types/providers/anthropic.d.ts +6 -59
  36. package/dist/types/providers/embeddings-server.d.ts +32 -0
  37. package/dist/types/providers/gitlab-duo.d.ts +0 -1
  38. package/dist/types/providers/images-server.d.ts +22 -0
  39. package/dist/types/providers/kimi.d.ts +1 -5
  40. package/dist/types/providers/openai-codex-attestation.d.ts +6 -0
  41. package/dist/types/providers/openai-codex-compaction.d.ts +6 -0
  42. package/dist/types/providers/openai-codex-responses.d.ts +3 -27
  43. package/dist/types/providers/openai-codex-transport.d.ts +7 -0
  44. package/dist/types/providers/openai-shared.d.ts +0 -9
  45. package/dist/types/providers/register-builtins.d.ts +20 -22
  46. package/dist/types/providers/rerank-server.d.ts +35 -0
  47. package/dist/types/providers/speech-server.d.ts +8 -0
  48. package/dist/types/providers/synthetic.d.ts +1 -5
  49. package/dist/types/providers/systemone-server.d.ts +26 -0
  50. package/dist/types/providers/transcriptions-server.d.ts +32 -0
  51. package/dist/types/providers/video-server.d.ts +39 -0
  52. package/dist/types/rerank/index.d.ts +7 -0
  53. package/dist/types/rerank/openrouter-rerank.d.ts +15 -0
  54. package/dist/types/rerank/types.d.ts +17 -0
  55. package/dist/types/speech/index.d.ts +13 -0
  56. package/dist/types/speech/openai-speech.d.ts +3 -0
  57. package/dist/types/speech/transport.d.ts +7 -0
  58. package/dist/types/speech/types.d.ts +24 -0
  59. package/dist/types/speech/xai-tts.d.ts +7 -0
  60. package/dist/types/transcription/index.d.ts +7 -0
  61. package/dist/types/transcription/openai-transcriptions.d.ts +15 -0
  62. package/dist/types/transcription/types.d.ts +41 -0
  63. package/dist/types/video/index.d.ts +11 -0
  64. package/dist/types/video/openrouter-video.d.ts +19 -0
  65. package/dist/types/video/types.d.ts +62 -0
  66. package/package.json +30 -6
  67. package/src/auth-gateway/dispatch.ts +273 -0
  68. package/src/auth-gateway/http.ts +6 -7
  69. package/src/auth-gateway/index.ts +1 -0
  70. package/src/auth-gateway/routes/embeddings.ts +98 -0
  71. package/src/auth-gateway/routes/images.ts +131 -0
  72. package/src/auth-gateway/routes/rerank.ts +87 -0
  73. package/src/auth-gateway/routes/speech.ts +101 -0
  74. package/src/auth-gateway/routes/systemone.ts +116 -0
  75. package/src/auth-gateway/routes/transcriptions.ts +98 -0
  76. package/src/auth-gateway/routes/video.ts +243 -0
  77. package/src/auth-gateway/server.ts +123 -260
  78. package/src/auth-retry.ts +3 -0
  79. package/src/auth-storage.ts +28 -22
  80. package/src/embeddings/index.ts +17 -0
  81. package/src/embeddings/openai-embeddings.ts +141 -0
  82. package/src/embeddings/types.ts +14 -0
  83. package/src/error/auth-classify.ts +10 -2
  84. package/src/error/classes.ts +23 -3
  85. package/src/error/flags.ts +51 -16
  86. package/src/error/rate-limit.ts +1 -1
  87. package/src/images/google-antigravity.ts +180 -0
  88. package/src/images/google-generative-ai.ts +92 -0
  89. package/src/images/index.ts +59 -0
  90. package/src/images/openai-hosted.ts +185 -0
  91. package/src/images/openai-images.ts +110 -0
  92. package/src/images/openrouter-images.ts +33 -0
  93. package/src/images/shared.ts +193 -0
  94. package/src/images/types.ts +36 -0
  95. package/src/index.ts +22 -13
  96. package/src/judgment/types.ts +6 -3
  97. package/src/judgment/typesafe.ts +58 -48
  98. package/src/provider-session-state.ts +1 -1
  99. package/src/providers/anthropic-compaction.ts +54 -0
  100. package/src/providers/anthropic-identity.ts +136 -0
  101. package/src/providers/anthropic-state.ts +55 -0
  102. package/src/providers/anthropic.ts +39 -296
  103. package/src/providers/bedrock-mantle.ts +1 -1
  104. package/src/providers/embeddings-server.ts +151 -0
  105. package/src/providers/gitlab-duo.ts +0 -4
  106. package/src/providers/images-server.ts +159 -0
  107. package/src/providers/kimi.ts +1 -8
  108. package/src/providers/openai-codex-attestation.ts +19 -0
  109. package/src/providers/openai-codex-compaction.ts +18 -0
  110. package/src/providers/openai-codex-responses.ts +9 -68
  111. package/src/providers/openai-codex-transport.ts +18 -0
  112. package/src/providers/openai-shared.ts +1 -10
  113. package/src/providers/register-builtins.ts +113 -320
  114. package/src/providers/rerank-server.ts +166 -0
  115. package/src/providers/speech-server.ts +53 -0
  116. package/src/providers/synthetic.ts +1 -8
  117. package/src/providers/systemone-server.ts +73 -0
  118. package/src/providers/transcriptions-server.ts +243 -0
  119. package/src/providers/video-server.ts +286 -0
  120. package/src/registry/cloudflare-ai-gateway.ts +1 -1
  121. package/src/rerank/index.ts +13 -0
  122. package/src/rerank/openrouter-rerank.ts +136 -0
  123. package/src/rerank/types.ts +20 -0
  124. package/src/speech/index.ts +35 -0
  125. package/src/speech/openai-speech.ts +26 -0
  126. package/src/speech/transport.ts +66 -0
  127. package/src/speech/types.ts +37 -0
  128. package/src/speech/xai-tts.ts +41 -0
  129. package/src/stream.ts +7 -15
  130. package/src/transcription/index.ts +17 -0
  131. package/src/transcription/openai-transcriptions.ts +133 -0
  132. package/src/transcription/types.ts +46 -0
  133. package/src/utils/anthropic-auth.ts +3 -6
  134. package/src/video/index.ts +34 -0
  135. package/src/video/openrouter-video.ts +210 -0
  136. 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
- const httpStatus = extractHttpStatusFromError(error);
45
- const message = error instanceof Error ? error.message : typeof error === "string" ? error : undefined;
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;
@@ -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
- return new AnthropicApiError(response.status, `${response.status} ${detail}`, response.headers);
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
 
@@ -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" && ACCOUNT_POLICY_PATTERN.test(link.code)) {
601
- kinds |= Flag.AccountPolicy | Flag.ContentBlocked;
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 === "object" &&
660
- "message" in link &&
661
- typeof (link as { message: unknown }).message === "string"
662
- ) {
663
- linkMessage = (link as { message: string }).message;
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: AssistantMessage, contextWindow?: number): boolean {
839
- if (!contextWindow) return false;
840
- const inputTokens = message.usage.input + message.usage.cacheRead + message.usage.cacheWrite;
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
- export function isContextOverflow(message: AssistantMessage, contextWindow?: number): boolean {
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: AssistantMessage | undefined,
899
+ message: ContextOverflowMessage | undefined,
865
900
  contextWindow?: number,
866
901
  ): boolean {
867
902
  const overflowFlagged =
@@ -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
+ }