@oh-my-pi/pi-ai 18.2.7 → 18.2.9

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 (133) hide show
  1. package/CHANGELOG.md +49 -18
  2. package/dist/types/auth-gateway/dispatch.d.ts +80 -0
  3. package/dist/types/auth-gateway/http.d.ts +5 -6
  4. package/dist/types/auth-gateway/index.d.ts +1 -0
  5. package/dist/types/auth-gateway/routes/embeddings.d.ts +3 -0
  6. package/dist/types/auth-gateway/routes/images.d.ts +3 -0
  7. package/dist/types/auth-gateway/routes/rerank.d.ts +3 -0
  8. package/dist/types/auth-gateway/routes/speech.d.ts +2 -0
  9. package/dist/types/auth-gateway/routes/systemone.d.ts +2 -0
  10. package/dist/types/auth-gateway/routes/transcriptions.d.ts +3 -0
  11. package/dist/types/auth-gateway/routes/video.d.ts +7 -0
  12. package/dist/types/auth-gateway/server.d.ts +10 -16
  13. package/dist/types/auth-gateway/types.d.ts +5 -0
  14. package/dist/types/auth-storage.d.ts +35 -32
  15. package/dist/types/embeddings/index.d.ts +7 -0
  16. package/dist/types/embeddings/openai-embeddings.d.ts +15 -0
  17. package/dist/types/embeddings/types.d.ts +15 -0
  18. package/dist/types/images/google-antigravity.d.ts +9 -0
  19. package/dist/types/images/google-generative-ai.d.ts +3 -0
  20. package/dist/types/images/index.d.ts +14 -0
  21. package/dist/types/images/openai-hosted.d.ts +3 -0
  22. package/dist/types/images/openai-images.d.ts +5 -0
  23. package/dist/types/images/openrouter-images.d.ts +3 -0
  24. package/dist/types/images/shared.d.ts +34 -0
  25. package/dist/types/images/types.d.ts +31 -0
  26. package/dist/types/index.d.ts +8 -1
  27. package/dist/types/judgment/typesafe.d.ts +2 -0
  28. package/dist/types/providers/amazon-bedrock.d.ts +7 -0
  29. package/dist/types/providers/claude-code-fingerprint.d.ts +22 -3
  30. package/dist/types/providers/embeddings-server.d.ts +32 -0
  31. package/dist/types/providers/google-gemini-cli.d.ts +0 -2
  32. package/dist/types/providers/images-server.d.ts +22 -0
  33. package/dist/types/providers/openai-chat-server-schema.d.ts +2 -2
  34. package/dist/types/providers/rerank-server.d.ts +35 -0
  35. package/dist/types/providers/speech-server.d.ts +8 -0
  36. package/dist/types/providers/systemone-server.d.ts +26 -0
  37. package/dist/types/providers/transcriptions-server.d.ts +32 -0
  38. package/dist/types/providers/video-server.d.ts +39 -0
  39. package/dist/types/rerank/index.d.ts +7 -0
  40. package/dist/types/rerank/openrouter-rerank.d.ts +15 -0
  41. package/dist/types/rerank/types.d.ts +17 -0
  42. package/dist/types/speech/index.d.ts +13 -0
  43. package/dist/types/speech/openai-speech.d.ts +3 -0
  44. package/dist/types/speech/transport.d.ts +7 -0
  45. package/dist/types/speech/types.d.ts +24 -0
  46. package/dist/types/speech/xai-tts.d.ts +7 -0
  47. package/dist/types/transcription/index.d.ts +7 -0
  48. package/dist/types/transcription/openai-transcriptions.d.ts +15 -0
  49. package/dist/types/transcription/types.d.ts +41 -0
  50. package/dist/types/usage/claude-api.d.ts +22 -0
  51. package/dist/types/usage/claude-reset.d.ts +44 -0
  52. package/dist/types/usage.d.ts +111 -5
  53. package/dist/types/utils/schema/json-schema-validator.d.ts +5 -2
  54. package/dist/types/utils/tool-call-loop-guard.d.ts +1 -1
  55. package/dist/types/video/index.d.ts +11 -0
  56. package/dist/types/video/openrouter-video.d.ts +19 -0
  57. package/dist/types/video/types.d.ts +62 -0
  58. package/package.json +30 -6
  59. package/src/auth/sqlite-credential-store.ts +44 -1
  60. package/src/auth-broker/remote-store.ts +6 -6
  61. package/src/auth-broker/wire-schemas.ts +14 -0
  62. package/src/auth-gateway/dispatch.ts +273 -0
  63. package/src/auth-gateway/http.ts +6 -7
  64. package/src/auth-gateway/index.ts +1 -0
  65. package/src/auth-gateway/routes/embeddings.ts +98 -0
  66. package/src/auth-gateway/routes/images.ts +131 -0
  67. package/src/auth-gateway/routes/rerank.ts +87 -0
  68. package/src/auth-gateway/routes/speech.ts +101 -0
  69. package/src/auth-gateway/routes/systemone.ts +116 -0
  70. package/src/auth-gateway/routes/transcriptions.ts +98 -0
  71. package/src/auth-gateway/routes/video.ts +243 -0
  72. package/src/auth-gateway/server.ts +127 -260
  73. package/src/auth-gateway/types.ts +5 -0
  74. package/src/auth-storage.ts +263 -140
  75. package/src/embeddings/index.ts +17 -0
  76. package/src/embeddings/openai-embeddings.ts +141 -0
  77. package/src/embeddings/types.ts +14 -0
  78. package/src/error/flags.ts +10 -0
  79. package/src/error/rate-limit.ts +1 -1
  80. package/src/images/google-antigravity.ts +180 -0
  81. package/src/images/google-generative-ai.ts +92 -0
  82. package/src/images/index.ts +59 -0
  83. package/src/images/openai-hosted.ts +185 -0
  84. package/src/images/openai-images.ts +110 -0
  85. package/src/images/openrouter-images.ts +33 -0
  86. package/src/images/shared.ts +193 -0
  87. package/src/images/types.ts +36 -0
  88. package/src/index.ts +8 -1
  89. package/src/judgment/typesafe.ts +5 -0
  90. package/src/providers/amazon-bedrock.ts +55 -5
  91. package/src/providers/anthropic.ts +45 -11
  92. package/src/providers/aws-credentials.ts +124 -11
  93. package/src/providers/claude-code-fingerprint.ts +55 -3
  94. package/src/providers/embeddings-server.ts +151 -0
  95. package/src/providers/gitlab-duo.ts +20 -4
  96. package/src/providers/google-gemini-cli.ts +0 -8
  97. package/src/providers/google-shared.ts +1 -18
  98. package/src/providers/images-server.ts +159 -0
  99. package/src/providers/openai-chat-server-schema.ts +1 -1
  100. package/src/providers/openai-chat-server.ts +3 -1
  101. package/src/providers/openai-codex-responses.ts +27 -5
  102. package/src/providers/openai-completions.ts +122 -19
  103. package/src/providers/pi-native-server.ts +1 -0
  104. package/src/providers/rerank-server.ts +166 -0
  105. package/src/providers/speech-server.ts +53 -0
  106. package/src/providers/systemone-server.ts +73 -0
  107. package/src/providers/transcriptions-server.ts +243 -0
  108. package/src/providers/video-server.ts +286 -0
  109. package/src/registry/oauth/anthropic.ts +2 -3
  110. package/src/rerank/index.ts +13 -0
  111. package/src/rerank/openrouter-rerank.ts +136 -0
  112. package/src/rerank/types.ts +20 -0
  113. package/src/speech/index.ts +35 -0
  114. package/src/speech/openai-speech.ts +26 -0
  115. package/src/speech/transport.ts +66 -0
  116. package/src/speech/types.ts +37 -0
  117. package/src/speech/xai-tts.ts +41 -0
  118. package/src/stream.ts +13 -4
  119. package/src/transcription/index.ts +17 -0
  120. package/src/transcription/openai-transcriptions.ts +133 -0
  121. package/src/transcription/types.ts +46 -0
  122. package/src/usage/alibaba-token-plan.ts +7 -1
  123. package/src/usage/claude-api.ts +66 -0
  124. package/src/usage/claude-reset.ts +638 -0
  125. package/src/usage/claude.ts +37 -59
  126. package/src/usage/kimi.ts +32 -1
  127. package/src/usage.ts +52 -5
  128. package/src/utils/schema/json-schema-validator.ts +23 -10
  129. package/src/utils/tool-call-loop-guard.ts +2 -2
  130. package/src/utils/validation.ts +145 -50
  131. package/src/video/index.ts +34 -0
  132. package/src/video/openrouter-video.ts +210 -0
  133. 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
+ }
@@ -275,6 +275,15 @@ const STRUCTURED_OUTPUTS_PATTERN = /structured[_ -]?outputs?/i;
275
275
  const FEATURE_NOT_SUPPORTED_PATTERN = /not (?:supported|available|enabled)|unsupported|does(?: not|n'?t) support/i;
276
276
  const ANTHROPIC_STRICT_FIELD_PATTERN = /\btools\.\d+\.custom\.strict\b/i;
277
277
  const EXTRA_INPUTS_NOT_PERMITTED_PATTERN = /extra inputs? (?:are|is) not permitted/i;
278
+ // Upstream strict-schema validation surfaced through a translating gateway.
279
+ // Vercel AI Gateway serves non-Anthropic upstreams on its Anthropic
280
+ // `/v1/messages` route and applies Anthropic's `strict: true` to an OpenAI
281
+ // function tool. OpenAI strict mode additionally demands every `properties`
282
+ // key in `required`, which Anthropic's strict subset does not, so a
283
+ // legally-optional parameter is rejected only after translation and the sole
284
+ // recovery is dropping `strict`. Mirrors the phrasings
285
+ // `shouldRetryWithoutStrictTools` already recognizes on the OpenAI-family path.
286
+ const STRICT_TOOL_SCHEMA_REJECTION_PATTERN = /invalid schema for function|invalid tool parameters schema/i;
278
287
  // Anthropic fast-mode unsupported: 400 rejecting `speed`, or 429 rate_limit_error
279
288
  // because the account lacks the extra-usage entitlement fast mode requires.
280
289
  const FAST_MODE_SPEED_PARAM_PATTERN = /\bspeed\b/i;
@@ -294,6 +303,7 @@ function matchesStrictToolsRejection(message: string, errorStatus: number | unde
294
303
  return true;
295
304
  }
296
305
  if (STRUCTURED_OUTPUTS_PATTERN.test(message) && FEATURE_NOT_SUPPORTED_PATTERN.test(message)) return true;
306
+ if (STRICT_TOOL_SCHEMA_REJECTION_PATTERN.test(message)) return true;
297
307
  if (!INVALID_REQUEST_PATTERN.test(message)) return false;
298
308
  const grammarTooLarge = GRAMMAR_TOO_LARGE_PATTERN.test(message) && GRAMMAR_TOO_LARGE_DETAIL_PATTERN.test(message);
299
309
  const schemaTooComplex =
@@ -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
+ }
@@ -0,0 +1,59 @@
1
+ import type { Api, Model } from "@oh-my-pi/pi-catalog/types";
2
+ import * as AIError from "../error";
3
+ import { generateAntigravityImage } from "./google-antigravity";
4
+ import { generateGoogleImage } from "./google-generative-ai";
5
+ import { generateOpenAIImage } from "./openai-images";
6
+ import { generateHostedImage } from "./openai-hosted";
7
+ import { generateOpenRouterImage } from "./openrouter-images";
8
+ import type { ImageGenerationOptions, ImageGenerationRequest, ImageGenerationResult } from "./types";
9
+
10
+ export * from "./google-antigravity";
11
+ export * from "./google-generative-ai";
12
+ export * from "./openai-hosted";
13
+ export * from "./openai-images";
14
+ export * from "./openrouter-images";
15
+ export * from "./types";
16
+
17
+ /** Catalog APIs {@link generateImage} serves; the hosted Responses pair needs an explicit carrier model. */
18
+ export type ImageGenerationApi =
19
+ | "openai-images"
20
+ | "openrouter-images"
21
+ | "google-generative-ai"
22
+ | "google-gemini-cli"
23
+ | "openai-responses"
24
+ | "openai-codex-responses";
25
+
26
+ /** Whether a catalog API generates images through one of the pi-ai image clients. */
27
+ export function isImageGenerationApi(api: Api): api is ImageGenerationApi {
28
+ return (
29
+ api === "openai-images" ||
30
+ api === "openrouter-images" ||
31
+ api === "google-generative-ai" ||
32
+ api === "google-gemini-cli" ||
33
+ api === "openai-responses" ||
34
+ api === "openai-codex-responses"
35
+ );
36
+ }
37
+
38
+ /** Generate (or edit, when `request.inputImages` is set) images through the transport selected by the model's `api`. */
39
+ export async function generateImage(
40
+ model: Model<Api>,
41
+ request: ImageGenerationRequest,
42
+ options: ImageGenerationOptions,
43
+ ): Promise<ImageGenerationResult> {
44
+ switch (model.api) {
45
+ case "openai-images":
46
+ return generateOpenAIImage(model, request, options);
47
+ case "openrouter-images":
48
+ return generateOpenRouterImage(model, request, options);
49
+ case "google-generative-ai":
50
+ return generateGoogleImage(model, request, options);
51
+ case "google-gemini-cli":
52
+ return generateAntigravityImage(model, request, options);
53
+ case "openai-responses":
54
+ case "openai-codex-responses":
55
+ return generateHostedImage(model, request, options);
56
+ default:
57
+ throw new AIError.ValidationError(`Image generation does not support model API ${model.api}`);
58
+ }
59
+ }