@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.
- package/CHANGELOG.md +49 -18
- 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-gateway/types.d.ts +5 -0
- package/dist/types/auth-storage.d.ts +35 -32
- 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/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 +8 -1
- package/dist/types/judgment/typesafe.d.ts +2 -0
- package/dist/types/providers/amazon-bedrock.d.ts +7 -0
- package/dist/types/providers/claude-code-fingerprint.d.ts +22 -3
- package/dist/types/providers/embeddings-server.d.ts +32 -0
- package/dist/types/providers/google-gemini-cli.d.ts +0 -2
- package/dist/types/providers/images-server.d.ts +22 -0
- package/dist/types/providers/openai-chat-server-schema.d.ts +2 -2
- package/dist/types/providers/rerank-server.d.ts +35 -0
- package/dist/types/providers/speech-server.d.ts +8 -0
- 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/usage/claude-api.d.ts +22 -0
- package/dist/types/usage/claude-reset.d.ts +44 -0
- package/dist/types/usage.d.ts +111 -5
- package/dist/types/utils/schema/json-schema-validator.d.ts +5 -2
- package/dist/types/utils/tool-call-loop-guard.d.ts +1 -1
- 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/sqlite-credential-store.ts +44 -1
- package/src/auth-broker/remote-store.ts +6 -6
- package/src/auth-broker/wire-schemas.ts +14 -0
- 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 +127 -260
- package/src/auth-gateway/types.ts +5 -0
- package/src/auth-storage.ts +263 -140
- 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/flags.ts +10 -0
- 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 +8 -1
- package/src/judgment/typesafe.ts +5 -0
- package/src/providers/amazon-bedrock.ts +55 -5
- package/src/providers/anthropic.ts +45 -11
- package/src/providers/aws-credentials.ts +124 -11
- package/src/providers/claude-code-fingerprint.ts +55 -3
- package/src/providers/embeddings-server.ts +151 -0
- package/src/providers/gitlab-duo.ts +20 -4
- package/src/providers/google-gemini-cli.ts +0 -8
- package/src/providers/google-shared.ts +1 -18
- package/src/providers/images-server.ts +159 -0
- package/src/providers/openai-chat-server-schema.ts +1 -1
- package/src/providers/openai-chat-server.ts +3 -1
- package/src/providers/openai-codex-responses.ts +27 -5
- package/src/providers/openai-completions.ts +122 -19
- package/src/providers/pi-native-server.ts +1 -0
- package/src/providers/rerank-server.ts +166 -0
- package/src/providers/speech-server.ts +53 -0
- 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/oauth/anthropic.ts +2 -3
- 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 +13 -4
- package/src/transcription/index.ts +17 -0
- package/src/transcription/openai-transcriptions.ts +133 -0
- package/src/transcription/types.ts +46 -0
- package/src/usage/alibaba-token-plan.ts +7 -1
- package/src/usage/claude-api.ts +66 -0
- package/src/usage/claude-reset.ts +638 -0
- package/src/usage/claude.ts +37 -59
- package/src/usage/kimi.ts +32 -1
- package/src/usage.ts +52 -5
- package/src/utils/schema/json-schema-validator.ts +23 -10
- package/src/utils/tool-call-loop-guard.ts +2 -2
- package/src/utils/validation.ts +145 -50
- 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,185 @@
|
|
|
1
|
+
import type { Api, Model } from "@oh-my-pi/pi-catalog/types";
|
|
2
|
+
import {
|
|
3
|
+
applyCodexResidencyHeader,
|
|
4
|
+
CODEX_BASE_URL,
|
|
5
|
+
getCodexAccountId,
|
|
6
|
+
OPENAI_HEADER_VALUES,
|
|
7
|
+
OPENAI_HEADERS,
|
|
8
|
+
URL_PATHS,
|
|
9
|
+
} from "@oh-my-pi/pi-catalog/wire/codex";
|
|
10
|
+
import { parseImageMetadata, readSseJson, USER_AGENT } from "@oh-my-pi/pi-utils";
|
|
11
|
+
import { withAuth } from "../auth-retry";
|
|
12
|
+
import * as AIError from "../error";
|
|
13
|
+
import { errorMessage, ImageApiError, modelHeaders, resolveOpenAIImageSize, toDataUrl, usageFromWire } from "./shared";
|
|
14
|
+
import type { GeneratedImage, ImageGenerationOptions, ImageGenerationRequest, ImageGenerationResult } from "./types";
|
|
15
|
+
|
|
16
|
+
const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
|
|
17
|
+
const IMAGE_SYSTEM_INSTRUCTION =
|
|
18
|
+
"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.";
|
|
19
|
+
|
|
20
|
+
interface HostedOutput {
|
|
21
|
+
type: "image_generation_call" | "message";
|
|
22
|
+
result?: string;
|
|
23
|
+
content?: Array<{ type?: string; text?: string; refusal?: string }>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface HostedResponse {
|
|
27
|
+
output?: HostedOutput[];
|
|
28
|
+
usage?: { input_tokens?: number; output_tokens?: number };
|
|
29
|
+
error?: { message?: string };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface HostedEvent {
|
|
33
|
+
type?: string;
|
|
34
|
+
item?: HostedOutput;
|
|
35
|
+
response?: HostedResponse;
|
|
36
|
+
error?: { message?: string };
|
|
37
|
+
message?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function responsesUrl(model: Model<Api>): string {
|
|
41
|
+
const fallback =
|
|
42
|
+
model.api === "openai-codex-responses" || model.provider === "openai-codex"
|
|
43
|
+
? CODEX_BASE_URL
|
|
44
|
+
: DEFAULT_OPENAI_BASE_URL;
|
|
45
|
+
const baseUrl = (model.baseUrl || fallback).replace(/\/+$/, "");
|
|
46
|
+
if (model.api !== "openai-codex-responses" && model.provider !== "openai-codex") {
|
|
47
|
+
return `${baseUrl}/responses`;
|
|
48
|
+
}
|
|
49
|
+
const baseWithSlash = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
50
|
+
return new URL(URL_PATHS.RESPONSES.slice(1), baseWithSlash)
|
|
51
|
+
.toString()
|
|
52
|
+
.replace(URL_PATHS.RESPONSES, URL_PATHS.CODEX_RESPONSES);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function headers(
|
|
56
|
+
carrier: Model<Api>,
|
|
57
|
+
key: string,
|
|
58
|
+
sessionId: string | undefined,
|
|
59
|
+
signal?: AbortSignal,
|
|
60
|
+
): Promise<Headers> {
|
|
61
|
+
const result = new Headers(await modelHeaders(carrier, signal));
|
|
62
|
+
result.set("Content-Type", "application/json");
|
|
63
|
+
result.set("Authorization", `Bearer ${key}`);
|
|
64
|
+
if (carrier.api === "openai-codex-responses" || carrier.provider === "openai-codex") {
|
|
65
|
+
const accountId = getCodexAccountId(key);
|
|
66
|
+
result.delete("x-api-key");
|
|
67
|
+
if (accountId) result.set(OPENAI_HEADERS.ACCOUNT_ID, accountId);
|
|
68
|
+
applyCodexResidencyHeader(result, key);
|
|
69
|
+
result.set(OPENAI_HEADERS.BETA, OPENAI_HEADER_VALUES.BETA_RESPONSES);
|
|
70
|
+
result.set(OPENAI_HEADERS.ORIGINATOR, OPENAI_HEADER_VALUES.ORIGINATOR_CODEX);
|
|
71
|
+
result.set("User-Agent", USER_AGENT);
|
|
72
|
+
if (sessionId) {
|
|
73
|
+
result.set(OPENAI_HEADERS.CONVERSATION_ID, sessionId);
|
|
74
|
+
result.set(OPENAI_HEADERS.SESSION_ID, sessionId);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function collectResponse(response: HostedResponse): ImageGenerationResult {
|
|
81
|
+
const images: GeneratedImage[] = [];
|
|
82
|
+
const texts: string[] = [];
|
|
83
|
+
for (const output of response.output ?? []) {
|
|
84
|
+
if (output.type === "image_generation_call" && output.result) {
|
|
85
|
+
const bytes = Buffer.from(output.result, "base64");
|
|
86
|
+
images.push({ data: output.result, mimeType: parseImageMetadata(bytes)?.mimeType ?? "image/webp" });
|
|
87
|
+
}
|
|
88
|
+
if (output.type === "message") {
|
|
89
|
+
for (const part of output.content ?? []) {
|
|
90
|
+
if (part.type === "output_text" && part.text) texts.push(part.text);
|
|
91
|
+
if (part.type === "refusal" && part.refusal) texts.push(part.refusal);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const text = texts.join("\n").trim();
|
|
96
|
+
return { images, ...(text ? { text } : {}), usage: usageFromWire(response.usage) };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function parseSse(response: Response, signal?: AbortSignal): Promise<ImageGenerationResult> {
|
|
100
|
+
if (!response.body) {
|
|
101
|
+
throw new AIError.ProviderResponseError("OpenAI hosted image response has no body", { kind: "empty-body" });
|
|
102
|
+
}
|
|
103
|
+
const fallbackOutput: HostedOutput[] = [];
|
|
104
|
+
let completed: HostedResponse | undefined;
|
|
105
|
+
for await (const event of readSseJson<HostedEvent>(response.body, signal)) {
|
|
106
|
+
if (event.type === "error") {
|
|
107
|
+
throw new AIError.ProviderResponseError(
|
|
108
|
+
event.error?.message ?? event.message ?? "OpenAI image request failed",
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
if (event.type === "response.failed") {
|
|
112
|
+
throw new AIError.ProviderResponseError(event.response?.error?.message ?? "OpenAI image request failed");
|
|
113
|
+
}
|
|
114
|
+
if (event.type === "response.output_item.done" && event.item) fallbackOutput.push(event.item);
|
|
115
|
+
if ((event.type === "response.completed" || event.type === "response.done") && event.response) {
|
|
116
|
+
completed = event.response;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return collectResponse(completed?.output?.length ? completed : { output: fallbackOutput, usage: completed?.usage });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function generateHostedImage(
|
|
123
|
+
model: Model,
|
|
124
|
+
request: ImageGenerationRequest,
|
|
125
|
+
options: ImageGenerationOptions,
|
|
126
|
+
): Promise<ImageGenerationResult> {
|
|
127
|
+
const carrier = options.carrier;
|
|
128
|
+
if (!carrier) throw new AIError.ValidationError("Hosted image generation requires an explicit carrier model");
|
|
129
|
+
if (carrier.api !== "openai-responses" && carrier.api !== "openai-codex-responses") {
|
|
130
|
+
throw new AIError.ValidationError(`Hosted image carrier API ${carrier.api} is unsupported`);
|
|
131
|
+
}
|
|
132
|
+
const stream = carrier.api === "openai-codex-responses";
|
|
133
|
+
const content: Array<Record<string, unknown>> = [{ type: "input_text", text: request.prompt }];
|
|
134
|
+
for (const image of request.inputImages ?? []) {
|
|
135
|
+
content.push({ type: "input_image", detail: "auto", image_url: toDataUrl(image) });
|
|
136
|
+
}
|
|
137
|
+
const size = resolveOpenAIImageSize(request.aspectRatio, request.imageSize);
|
|
138
|
+
const tool = {
|
|
139
|
+
type: "image_generation",
|
|
140
|
+
action: content.length > 1 ? "edit" : "generate",
|
|
141
|
+
output_format: "webp",
|
|
142
|
+
...(size ? { size } : {}),
|
|
143
|
+
...(model.api === "openai-responses" ? { model: model.requestModelId ?? model.id } : {}),
|
|
144
|
+
};
|
|
145
|
+
const body = {
|
|
146
|
+
model: carrier.requestModelId ?? carrier.id,
|
|
147
|
+
input: [{ role: "user", content }],
|
|
148
|
+
tools: [tool],
|
|
149
|
+
tool_choice: { type: "image_generation" },
|
|
150
|
+
store: false,
|
|
151
|
+
...(stream ? { instructions: IMAGE_SYSTEM_INSTRUCTION, stream: true } : {}),
|
|
152
|
+
};
|
|
153
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
154
|
+
return withAuth(
|
|
155
|
+
options.apiKey,
|
|
156
|
+
async key => {
|
|
157
|
+
const response = await fetchImpl(responsesUrl(carrier), {
|
|
158
|
+
method: "POST",
|
|
159
|
+
headers: await headers(carrier, key, options.sessionId, options.signal),
|
|
160
|
+
body: JSON.stringify(body),
|
|
161
|
+
signal: options.signal,
|
|
162
|
+
});
|
|
163
|
+
if (!response.ok) {
|
|
164
|
+
const text = await response.text();
|
|
165
|
+
throw new ImageApiError(
|
|
166
|
+
`OpenAI image request failed (${response.status}): ${errorMessage(text)}`,
|
|
167
|
+
response.status,
|
|
168
|
+
{ headers: response.headers },
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
if (stream || response.headers.get("content-type")?.includes("text/event-stream")) {
|
|
172
|
+
return parseSse(response, options.signal);
|
|
173
|
+
}
|
|
174
|
+
const value = (await response.json()) as HostedResponse;
|
|
175
|
+
if (value.error) throw new AIError.ProviderResponseError(value.error.message ?? "OpenAI image request failed");
|
|
176
|
+
if (!Array.isArray(value.output)) {
|
|
177
|
+
throw new AIError.ProviderResponseError("OpenAI image request returned a malformed response", {
|
|
178
|
+
kind: "envelope",
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
return collectResponse(value);
|
|
182
|
+
},
|
|
183
|
+
{ signal: options.signal },
|
|
184
|
+
);
|
|
185
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { Model } from "@oh-my-pi/pi-catalog/types";
|
|
2
|
+
import * as AIError from "../error";
|
|
3
|
+
import {
|
|
4
|
+
decodeImageResponse,
|
|
5
|
+
imageBaseUrl,
|
|
6
|
+
postJson,
|
|
7
|
+
postMultipart,
|
|
8
|
+
resolveOpenAIImageSize,
|
|
9
|
+
toDataUrl,
|
|
10
|
+
} from "./shared";
|
|
11
|
+
import type { ImageGenerationOptions, ImageGenerationRequest, ImageGenerationResult } from "./types";
|
|
12
|
+
|
|
13
|
+
export const XAI_MAX_EDIT_IMAGES = 3;
|
|
14
|
+
|
|
15
|
+
export function resolveXAIResolution(imageSize?: string): "1k" | "2k" {
|
|
16
|
+
return !imageSize || imageSize === "1024x1024" ? "1k" : "2k";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function generateOpenAIImage(
|
|
20
|
+
model: Model,
|
|
21
|
+
request: ImageGenerationRequest,
|
|
22
|
+
options: ImageGenerationOptions,
|
|
23
|
+
): Promise<ImageGenerationResult> {
|
|
24
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
25
|
+
const size = resolveOpenAIImageSize(request.aspectRatio, request.imageSize);
|
|
26
|
+
const count = request.count ?? 1;
|
|
27
|
+
const isXAI = model.provider === "xai" || model.provider === "xai-oauth";
|
|
28
|
+
const generationBody = isXAI
|
|
29
|
+
? {
|
|
30
|
+
model: model.requestModelId ?? model.id,
|
|
31
|
+
prompt: request.prompt,
|
|
32
|
+
aspect_ratio: request.aspectRatio ?? "1:1",
|
|
33
|
+
resolution: resolveXAIResolution(request.imageSize),
|
|
34
|
+
n: count,
|
|
35
|
+
response_format: "b64_json",
|
|
36
|
+
}
|
|
37
|
+
: {
|
|
38
|
+
model: model.requestModelId ?? model.id,
|
|
39
|
+
prompt: request.prompt,
|
|
40
|
+
n: count,
|
|
41
|
+
response_format: "b64_json",
|
|
42
|
+
...(size ? { size } : {}),
|
|
43
|
+
};
|
|
44
|
+
const references = (request.inputImages ?? []).map(image => ({ type: "image_url", url: toDataUrl(image) }));
|
|
45
|
+
if (isXAI && references.length > XAI_MAX_EDIT_IMAGES) {
|
|
46
|
+
throw new AIError.ValidationError(
|
|
47
|
+
`${model.provider} image edits accept up to ${XAI_MAX_EDIT_IMAGES} reference images; got ${references.length}`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
const [firstReference, ...remainingReferences] = references;
|
|
51
|
+
const body = isXAI
|
|
52
|
+
? remainingReferences.length === 0
|
|
53
|
+
? { ...generationBody, image: firstReference }
|
|
54
|
+
: { ...generationBody, images: references }
|
|
55
|
+
: { ...generationBody, input_references: references };
|
|
56
|
+
const baseUrl = imageBaseUrl(model);
|
|
57
|
+
let response: unknown;
|
|
58
|
+
if (references.length === 0) {
|
|
59
|
+
response = await postJson({
|
|
60
|
+
model,
|
|
61
|
+
url: `${baseUrl}/images/generations`,
|
|
62
|
+
body: generationBody,
|
|
63
|
+
apiKey: options.apiKey,
|
|
64
|
+
fetch: fetchImpl,
|
|
65
|
+
signal: options.signal,
|
|
66
|
+
});
|
|
67
|
+
} else {
|
|
68
|
+
try {
|
|
69
|
+
if (model.provider === "openai") {
|
|
70
|
+
const form = new FormData();
|
|
71
|
+
form.set("model", model.requestModelId ?? model.id);
|
|
72
|
+
form.set("prompt", request.prompt);
|
|
73
|
+
form.set("n", String(count));
|
|
74
|
+
form.set("response_format", "b64_json");
|
|
75
|
+
if (size) form.set("size", size);
|
|
76
|
+
for (const image of request.inputImages ?? []) {
|
|
77
|
+
form.append("image", new File([Buffer.from(image.data, "base64")], "image", { type: image.mimeType }));
|
|
78
|
+
}
|
|
79
|
+
response = await postMultipart({
|
|
80
|
+
model,
|
|
81
|
+
url: `${baseUrl}/images/edits`,
|
|
82
|
+
body: form,
|
|
83
|
+
apiKey: options.apiKey,
|
|
84
|
+
fetch: fetchImpl,
|
|
85
|
+
signal: options.signal,
|
|
86
|
+
});
|
|
87
|
+
} else {
|
|
88
|
+
response = await postJson({
|
|
89
|
+
model,
|
|
90
|
+
url: `${baseUrl}/images/edits`,
|
|
91
|
+
body,
|
|
92
|
+
apiKey: options.apiKey,
|
|
93
|
+
fetch: fetchImpl,
|
|
94
|
+
signal: options.signal,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (!(error instanceof AIError.ProviderHttpError) || error.status !== 404) throw error;
|
|
99
|
+
response = await postJson({
|
|
100
|
+
model,
|
|
101
|
+
url: `${baseUrl}/images/generations`,
|
|
102
|
+
body,
|
|
103
|
+
apiKey: options.apiKey,
|
|
104
|
+
fetch: fetchImpl,
|
|
105
|
+
signal: options.signal,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return decodeImageResponse(response, fetchImpl, options.signal);
|
|
110
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Model } from "@oh-my-pi/pi-catalog/types";
|
|
2
|
+
import { decodeImageResponse, imageBaseUrl, postJson, toDataUrl } from "./shared";
|
|
3
|
+
import type { ImageGenerationOptions, ImageGenerationRequest, ImageGenerationResult } from "./types";
|
|
4
|
+
|
|
5
|
+
export async function generateOpenRouterImage(
|
|
6
|
+
model: Model,
|
|
7
|
+
request: ImageGenerationRequest,
|
|
8
|
+
options: ImageGenerationOptions,
|
|
9
|
+
): Promise<ImageGenerationResult> {
|
|
10
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
11
|
+
const inputReferences = (request.inputImages ?? []).map(image => ({
|
|
12
|
+
type: "image_url",
|
|
13
|
+
image_url: { url: toDataUrl(image) },
|
|
14
|
+
}));
|
|
15
|
+
const body = {
|
|
16
|
+
model: model.requestModelId ?? model.id,
|
|
17
|
+
prompt: request.prompt,
|
|
18
|
+
n: request.count ?? 1,
|
|
19
|
+
response_format: "b64_json",
|
|
20
|
+
...(request.aspectRatio ? { aspect_ratio: request.aspectRatio } : {}),
|
|
21
|
+
...(request.imageSize ? { image_size: request.imageSize } : {}),
|
|
22
|
+
...(inputReferences.length > 0 ? { input_references: inputReferences } : {}),
|
|
23
|
+
};
|
|
24
|
+
const response = await postJson({
|
|
25
|
+
model,
|
|
26
|
+
url: `${imageBaseUrl(model)}/images`,
|
|
27
|
+
body,
|
|
28
|
+
apiKey: options.apiKey,
|
|
29
|
+
fetch: fetchImpl,
|
|
30
|
+
signal: options.signal,
|
|
31
|
+
});
|
|
32
|
+
return decodeImageResponse(response, fetchImpl, options.signal);
|
|
33
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import type { FetchImpl, Model, Usage } from "@oh-my-pi/pi-catalog/types";
|
|
2
|
+
import { parseImageMetadata, USER_AGENT } from "@oh-my-pi/pi-utils";
|
|
3
|
+
import type { ApiKey } from "../auth-retry";
|
|
4
|
+
import { withAuth } from "../auth-retry";
|
|
5
|
+
import * as AIError from "../error";
|
|
6
|
+
import type { GeneratedImage } from "./types";
|
|
7
|
+
|
|
8
|
+
export class ImageApiError extends AIError.ProviderHttpError {
|
|
9
|
+
override readonly name = "ImageApiError";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function emptyUsage(input = 0, output = 0, cost = 0): Usage {
|
|
13
|
+
return {
|
|
14
|
+
input,
|
|
15
|
+
output,
|
|
16
|
+
cacheRead: 0,
|
|
17
|
+
cacheWrite: 0,
|
|
18
|
+
totalTokens: input + output,
|
|
19
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: cost },
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function usageFromWire(value: unknown): Usage {
|
|
24
|
+
if (value === null || typeof value !== "object") return emptyUsage();
|
|
25
|
+
const usage = value as Record<string, unknown>;
|
|
26
|
+
const number = (key: string): number => (typeof usage[key] === "number" ? usage[key] : 0);
|
|
27
|
+
return emptyUsage(
|
|
28
|
+
number("input_tokens") || number("prompt_tokens"),
|
|
29
|
+
number("output_tokens") || number("completion_tokens"),
|
|
30
|
+
number("cost"),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function imageBaseUrl(model: Model): string {
|
|
35
|
+
if (!model.baseUrl) throw new AIError.ValidationError(`Image model ${model.provider}/${model.id} has no base URL`);
|
|
36
|
+
return model.baseUrl.replace(/\/+$/, "");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function modelHeaders(model: Model, signal?: AbortSignal): Promise<Record<string, string>> {
|
|
40
|
+
return { ...model.headers, ...(await model.resolveHeaders?.(signal)) };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function errorMessage(rawText: string): string {
|
|
44
|
+
try {
|
|
45
|
+
const parsed = JSON.parse(rawText) as { detail?: string; error?: { message?: string } };
|
|
46
|
+
return parsed.detail ?? parsed.error?.message ?? rawText;
|
|
47
|
+
} catch {
|
|
48
|
+
return rawText;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function parseImageApiResponse(model: Model, response: Response): Promise<unknown> {
|
|
53
|
+
const text = await response.text();
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
throw new ImageApiError(
|
|
56
|
+
`${model.provider}/${model.id} image request failed (${response.status}): ${errorMessage(text)}`,
|
|
57
|
+
response.status,
|
|
58
|
+
{ headers: response.headers },
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
return JSON.parse(text) as unknown;
|
|
63
|
+
} catch (cause) {
|
|
64
|
+
throw new AIError.ProviderResponseError("Image API returned malformed JSON", {
|
|
65
|
+
provider: model.provider,
|
|
66
|
+
kind: "envelope",
|
|
67
|
+
cause,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function postJson(options: {
|
|
73
|
+
model: Model;
|
|
74
|
+
url: string;
|
|
75
|
+
body: unknown;
|
|
76
|
+
apiKey: ApiKey;
|
|
77
|
+
fetch: FetchImpl;
|
|
78
|
+
signal?: AbortSignal;
|
|
79
|
+
}): Promise<unknown> {
|
|
80
|
+
return withAuth(
|
|
81
|
+
options.apiKey,
|
|
82
|
+
async key => {
|
|
83
|
+
const response = await options.fetch(options.url, {
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers: {
|
|
86
|
+
...(await modelHeaders(options.model, options.signal)),
|
|
87
|
+
Authorization: `Bearer ${key}`,
|
|
88
|
+
"Content-Type": "application/json",
|
|
89
|
+
"User-Agent": USER_AGENT,
|
|
90
|
+
},
|
|
91
|
+
body: JSON.stringify(options.body),
|
|
92
|
+
signal: options.signal,
|
|
93
|
+
});
|
|
94
|
+
return parseImageApiResponse(options.model, response);
|
|
95
|
+
},
|
|
96
|
+
{ signal: options.signal },
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function postMultipart(options: {
|
|
101
|
+
model: Model;
|
|
102
|
+
url: string;
|
|
103
|
+
body: FormData;
|
|
104
|
+
apiKey: ApiKey;
|
|
105
|
+
fetch: FetchImpl;
|
|
106
|
+
signal?: AbortSignal;
|
|
107
|
+
}): Promise<unknown> {
|
|
108
|
+
return withAuth(
|
|
109
|
+
options.apiKey,
|
|
110
|
+
async key => {
|
|
111
|
+
const response = await options.fetch(options.url, {
|
|
112
|
+
method: "POST",
|
|
113
|
+
headers: {
|
|
114
|
+
...(await modelHeaders(options.model, options.signal)),
|
|
115
|
+
Authorization: `Bearer ${key}`,
|
|
116
|
+
"User-Agent": USER_AGENT,
|
|
117
|
+
},
|
|
118
|
+
body: options.body,
|
|
119
|
+
signal: options.signal,
|
|
120
|
+
});
|
|
121
|
+
return parseImageApiResponse(options.model, response);
|
|
122
|
+
},
|
|
123
|
+
{ signal: options.signal },
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function imageFromUrl(url: string, fetch: FetchImpl, signal?: AbortSignal): Promise<GeneratedImage> {
|
|
128
|
+
const response = await fetch(url, { signal });
|
|
129
|
+
if (!response.ok) {
|
|
130
|
+
const text = await response.text();
|
|
131
|
+
throw new ImageApiError(`Image download failed (${response.status}): ${text}`, response.status, {
|
|
132
|
+
headers: response.headers,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
const mimeType = response.headers.get("content-type")?.split(";")[0];
|
|
136
|
+
if (!mimeType?.startsWith("image/")) {
|
|
137
|
+
throw new AIError.ProviderResponseError(`Image URL returned unsupported content type: ${mimeType ?? "missing"}`, {
|
|
138
|
+
kind: "envelope",
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
142
|
+
return { data: bytes.toBase64(), mimeType };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function decodeImageResponse(
|
|
146
|
+
value: unknown,
|
|
147
|
+
fetch: FetchImpl,
|
|
148
|
+
signal?: AbortSignal,
|
|
149
|
+
): Promise<{ images: GeneratedImage[]; usage: Usage }> {
|
|
150
|
+
if (value === null || typeof value !== "object") {
|
|
151
|
+
throw new AIError.ProviderResponseError("Image API returned a malformed response", { kind: "envelope" });
|
|
152
|
+
}
|
|
153
|
+
const root = value as { data?: unknown; usage?: unknown };
|
|
154
|
+
if (!Array.isArray(root.data)) {
|
|
155
|
+
throw new AIError.ProviderResponseError("Image API response is missing data", { kind: "envelope" });
|
|
156
|
+
}
|
|
157
|
+
const images: GeneratedImage[] = [];
|
|
158
|
+
for (const item of root.data) {
|
|
159
|
+
if (item === null || typeof item !== "object") continue;
|
|
160
|
+
const image = item as { b64_json?: unknown; url?: unknown; media_type?: unknown };
|
|
161
|
+
if (typeof image.b64_json === "string" && image.b64_json.length > 0) {
|
|
162
|
+
const bytes = Buffer.from(image.b64_json, "base64");
|
|
163
|
+
const mimeType =
|
|
164
|
+
typeof image.media_type === "string"
|
|
165
|
+
? image.media_type
|
|
166
|
+
: (parseImageMetadata(bytes)?.mimeType ?? "image/png");
|
|
167
|
+
images.push({ data: image.b64_json, mimeType });
|
|
168
|
+
} else if (typeof image.url === "string" && image.url.length > 0) {
|
|
169
|
+
images.push(await imageFromUrl(image.url, fetch, signal));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return { images, usage: usageFromWire(root.usage) };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function toDataUrl(image: GeneratedImage): string {
|
|
176
|
+
return `data:${image.mimeType};base64,${image.data}`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function resolveOpenAIImageSize(aspectRatio?: string, imageSize?: string): string | undefined {
|
|
180
|
+
if (imageSize) return imageSize;
|
|
181
|
+
switch (aspectRatio) {
|
|
182
|
+
case "1:1":
|
|
183
|
+
return "1024x1024";
|
|
184
|
+
case "3:4":
|
|
185
|
+
case "9:16":
|
|
186
|
+
return "1024x1536";
|
|
187
|
+
case "4:3":
|
|
188
|
+
case "16:9":
|
|
189
|
+
return "1536x1024";
|
|
190
|
+
default:
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Api, FetchImpl, Model, Usage } from "@oh-my-pi/pi-catalog/types";
|
|
2
|
+
import type { ApiKey } from "../auth-retry";
|
|
3
|
+
|
|
4
|
+
export interface ImageInput {
|
|
5
|
+
data: string;
|
|
6
|
+
mimeType: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ImageGenerationRequest {
|
|
10
|
+
prompt: string;
|
|
11
|
+
inputImages?: ImageInput[];
|
|
12
|
+
aspectRatio?: string;
|
|
13
|
+
imageSize?: string;
|
|
14
|
+
count?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface GeneratedImage {
|
|
18
|
+
data: string;
|
|
19
|
+
mimeType: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ImageGenerationResult {
|
|
23
|
+
images: GeneratedImage[];
|
|
24
|
+
text?: string;
|
|
25
|
+
usage: Usage;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ImageGenerationOptions {
|
|
29
|
+
apiKey: ApiKey;
|
|
30
|
+
fetch?: FetchImpl;
|
|
31
|
+
signal?: AbortSignal;
|
|
32
|
+
/** Chat model that carries a Responses `image_generation` tool call. */
|
|
33
|
+
carrier?: Model<Api>;
|
|
34
|
+
/** Stable provider session id, used by the Codex Responses carrier. */
|
|
35
|
+
sessionId?: string;
|
|
36
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
export { type Type, type } from "@oh-my-pi/omptype";
|
|
2
2
|
export * from "./api-registry";
|
|
3
3
|
export type * from "./auth-broker";
|
|
4
|
-
export type { AuthGatewayBootOptions, ModelResolver } from "./auth-gateway/
|
|
4
|
+
export type { AuthGatewayBootOptions, ModelResolver } from "./auth-gateway/dispatch";
|
|
5
5
|
export * from "./auth-gateway/types";
|
|
6
6
|
export * from "./auth-retry";
|
|
7
7
|
export * from "./auth-storage";
|
|
8
8
|
export * from "./error/rate-limit";
|
|
9
|
+
export * from "./embeddings";
|
|
10
|
+
export * from "./images";
|
|
9
11
|
export * from "./judgment";
|
|
12
|
+
export * from "./rerank";
|
|
13
|
+
export * from "./speech";
|
|
14
|
+
export * from "./transcription";
|
|
15
|
+
export * from "./video";
|
|
10
16
|
export * from "./oneshot-retry";
|
|
11
17
|
export * from "./provider-details";
|
|
12
18
|
export * from "./provider-session-state";
|
|
@@ -34,6 +40,7 @@ export * from "./stream";
|
|
|
34
40
|
export * from "./types";
|
|
35
41
|
export * from "./usage";
|
|
36
42
|
export * from "./usage/claude";
|
|
43
|
+
export * from "./usage/claude-reset";
|
|
37
44
|
export * from "./usage/cursor";
|
|
38
45
|
export * from "./usage/gemini";
|
|
39
46
|
export * from "./usage/github-copilot";
|
package/src/judgment/typesafe.ts
CHANGED
|
@@ -66,6 +66,8 @@ export interface TypeSafeJudgeOptions {
|
|
|
66
66
|
baseUrl?: string;
|
|
67
67
|
/** Defaults to {@link typesafeModel}. */
|
|
68
68
|
model?: string;
|
|
69
|
+
/** Static headers attached to judgment requests (e.g. proxy routing, gateway auth). */
|
|
70
|
+
headers?: Record<string, string>;
|
|
69
71
|
fetch?: FetchImpl;
|
|
70
72
|
/** Per-attempt timeout; defaults to {@link DEFAULT_TIMEOUT_MS}. */
|
|
71
73
|
timeoutMs?: number;
|
|
@@ -102,6 +104,7 @@ export class TypeSafeJudge implements Judge {
|
|
|
102
104
|
readonly model: string;
|
|
103
105
|
readonly baseUrl: string;
|
|
104
106
|
readonly #apiKey: ApiKey;
|
|
107
|
+
readonly #headers: Record<string, string> | undefined;
|
|
105
108
|
readonly #fetch: FetchImpl;
|
|
106
109
|
readonly #timeoutMs: number;
|
|
107
110
|
|
|
@@ -111,6 +114,7 @@ export class TypeSafeJudge implements Judge {
|
|
|
111
114
|
this.provider = options.provider ?? TYPESAFE_PROVIDER;
|
|
112
115
|
this.baseUrl = (options.baseUrl ?? typesafeBaseUrl()).replace(/\/+$/, "");
|
|
113
116
|
this.model = options.model ?? typesafeModel();
|
|
117
|
+
this.#headers = options.headers;
|
|
114
118
|
this.#fetch = options.fetch ?? fetch;
|
|
115
119
|
this.#timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
116
120
|
this.label = `${this.provider}/${this.model}`;
|
|
@@ -145,6 +149,7 @@ export class TypeSafeJudge implements Judge {
|
|
|
145
149
|
async #attempt<T>(path: string, body: string, key: string, signal: AbortSignal | undefined): Promise<T> {
|
|
146
150
|
const url = `${this.baseUrl}${path}`;
|
|
147
151
|
const headers: Record<string, string> = {
|
|
152
|
+
...this.#headers,
|
|
148
153
|
Authorization: `Bearer ${key}`,
|
|
149
154
|
Accept: "application/json",
|
|
150
155
|
"Content-Type": "application/json",
|