@oh-my-pi/pi-ai 18.2.7 → 18.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- 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/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 +7 -1
- package/dist/types/judgment/typesafe.d.ts +2 -0
- package/dist/types/providers/embeddings-server.d.ts +32 -0
- package/dist/types/providers/images-server.d.ts +22 -0
- 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/video/index.d.ts +11 -0
- package/dist/types/video/openrouter-video.d.ts +19 -0
- package/dist/types/video/types.d.ts +62 -0
- package/package.json +30 -6
- package/src/auth-gateway/dispatch.ts +273 -0
- package/src/auth-gateway/http.ts +6 -7
- package/src/auth-gateway/index.ts +1 -0
- package/src/auth-gateway/routes/embeddings.ts +98 -0
- package/src/auth-gateway/routes/images.ts +131 -0
- package/src/auth-gateway/routes/rerank.ts +87 -0
- package/src/auth-gateway/routes/speech.ts +101 -0
- package/src/auth-gateway/routes/systemone.ts +116 -0
- package/src/auth-gateway/routes/transcriptions.ts +98 -0
- package/src/auth-gateway/routes/video.ts +243 -0
- package/src/auth-gateway/server.ts +123 -260
- package/src/embeddings/index.ts +17 -0
- package/src/embeddings/openai-embeddings.ts +141 -0
- package/src/embeddings/types.ts +14 -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 +7 -1
- package/src/judgment/typesafe.ts +5 -0
- package/src/providers/embeddings-server.ts +151 -0
- package/src/providers/images-server.ts +159 -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/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/transcription/index.ts +17 -0
- package/src/transcription/openai-transcriptions.ts +133 -0
- package/src/transcription/types.ts +46 -0
- 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,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";
|
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",
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { type } from "@oh-my-pi/omptype";
|
|
2
|
+
import * as AIError from "../error";
|
|
3
|
+
import type { EmbeddingRequest, EmbeddingResult } from "../embeddings/types";
|
|
4
|
+
|
|
5
|
+
export const MAX_EMBEDDINGS_BODY_BYTES = 8 * 1024 * 1024;
|
|
6
|
+
|
|
7
|
+
const embeddingRequestSchema = type({
|
|
8
|
+
model: "string > 0",
|
|
9
|
+
input: "unknown",
|
|
10
|
+
"dimensions?": "unknown",
|
|
11
|
+
"encoding_format?": "unknown",
|
|
12
|
+
"user?": "unknown",
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export class EmbeddingsWireError extends AIError.ValidationError {
|
|
16
|
+
readonly status: number;
|
|
17
|
+
|
|
18
|
+
constructor(status: number, message: string, options?: { cause?: unknown }) {
|
|
19
|
+
super(message, options);
|
|
20
|
+
this.name = "EmbeddingsWireError";
|
|
21
|
+
this.status = status;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface EmbeddingsParsedRequest {
|
|
26
|
+
modelId: string;
|
|
27
|
+
request: EmbeddingRequest;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function invalid(message: string): never {
|
|
31
|
+
throw new EmbeddingsWireError(400, `embeddings: ${message}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseInput(value: unknown): EmbeddingRequest["input"] {
|
|
35
|
+
if (typeof value === "string") {
|
|
36
|
+
if (value.length === 0) invalid("input must not be empty");
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
if (!Array.isArray(value) || value.length === 0) invalid("input must be a non-empty string or array");
|
|
40
|
+
if (value.every(item => typeof item === "string" && item.length > 0)) return value;
|
|
41
|
+
if (value.every(item => typeof item === "number" && Number.isFinite(item))) return value;
|
|
42
|
+
if (
|
|
43
|
+
value.every(
|
|
44
|
+
item =>
|
|
45
|
+
Array.isArray(item) &&
|
|
46
|
+
item.length > 0 &&
|
|
47
|
+
item.every(token => typeof token === "number" && Number.isFinite(token)),
|
|
48
|
+
)
|
|
49
|
+
) {
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
invalid("input arrays must contain non-empty strings, finite numbers, or non-empty arrays of finite numbers");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function readBody(req: Request): Promise<Uint8Array> {
|
|
56
|
+
const contentLength = req.headers.get("content-length");
|
|
57
|
+
if (contentLength !== null) {
|
|
58
|
+
const declared = Number(contentLength);
|
|
59
|
+
if (Number.isFinite(declared) && declared > MAX_EMBEDDINGS_BODY_BYTES) {
|
|
60
|
+
throw new EmbeddingsWireError(413, "Request payload exceeds the 8 MiB limit");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const reader = req.body?.getReader();
|
|
64
|
+
if (!reader) return new Uint8Array();
|
|
65
|
+
const chunks: Uint8Array[] = [];
|
|
66
|
+
let total = 0;
|
|
67
|
+
for (;;) {
|
|
68
|
+
const { done, value } = await reader.read();
|
|
69
|
+
if (done) break;
|
|
70
|
+
total += value.byteLength;
|
|
71
|
+
if (total > MAX_EMBEDDINGS_BODY_BYTES) {
|
|
72
|
+
await reader.cancel();
|
|
73
|
+
throw new EmbeddingsWireError(413, "Request payload exceeds the 8 MiB limit");
|
|
74
|
+
}
|
|
75
|
+
chunks.push(value);
|
|
76
|
+
}
|
|
77
|
+
if (chunks.length === 1) return chunks[0]!;
|
|
78
|
+
const bytes = new Uint8Array(total);
|
|
79
|
+
let offset = 0;
|
|
80
|
+
for (const chunk of chunks) {
|
|
81
|
+
bytes.set(chunk, offset);
|
|
82
|
+
offset += chunk.byteLength;
|
|
83
|
+
}
|
|
84
|
+
return bytes;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Parse an OpenAI-compatible embeddings request with a bounded JSON body. */
|
|
88
|
+
export async function parseRequest(req: Request): Promise<EmbeddingsParsedRequest> {
|
|
89
|
+
const contentType = req.headers.get("content-type")?.toLowerCase() ?? "";
|
|
90
|
+
if (!contentType.startsWith("application/json")) {
|
|
91
|
+
throw new EmbeddingsWireError(400, "Content-Type must be application/json");
|
|
92
|
+
}
|
|
93
|
+
const bytes = await readBody(req);
|
|
94
|
+
let body: unknown;
|
|
95
|
+
try {
|
|
96
|
+
body = JSON.parse(new TextDecoder().decode(bytes));
|
|
97
|
+
} catch (error) {
|
|
98
|
+
throw new EmbeddingsWireError(400, "Invalid JSON body", { cause: error });
|
|
99
|
+
}
|
|
100
|
+
const parsed = embeddingRequestSchema(body);
|
|
101
|
+
if (parsed instanceof type.errors) throw new EmbeddingsWireError(400, `embeddings: ${parsed.summary}`);
|
|
102
|
+
const dimensions = parsed.dimensions;
|
|
103
|
+
if (dimensions !== undefined && (!Number.isInteger(dimensions) || (dimensions as number) < 1)) {
|
|
104
|
+
invalid("dimensions must be a positive integer");
|
|
105
|
+
}
|
|
106
|
+
const encodingFormat = parsed.encoding_format ?? "float";
|
|
107
|
+
if (encodingFormat !== "float" && encodingFormat !== "base64") {
|
|
108
|
+
invalid('encoding_format must be "float" or "base64"');
|
|
109
|
+
}
|
|
110
|
+
if (parsed.user !== undefined && (typeof parsed.user !== "string" || parsed.user.length === 0)) {
|
|
111
|
+
invalid("user must be a non-empty string");
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
modelId: parsed.model,
|
|
115
|
+
request: {
|
|
116
|
+
input: parseInput(parsed.input),
|
|
117
|
+
encodingFormat,
|
|
118
|
+
...(dimensions !== undefined && { dimensions: dimensions as number }),
|
|
119
|
+
...(parsed.user !== undefined && { user: parsed.user }),
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface EmbeddingsResponseBody {
|
|
125
|
+
object: "list";
|
|
126
|
+
data: Array<{ object: "embedding"; index: number; embedding: number[] | string }>;
|
|
127
|
+
model: string;
|
|
128
|
+
usage: { prompt_tokens: number; total_tokens: number; cost?: number };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Encode a canonical result as an OpenAI/OpenRouter embeddings response. */
|
|
132
|
+
export function encodeResponse(result: EmbeddingResult, requestedModelId: string): EmbeddingsResponseBody {
|
|
133
|
+
const billedCost = result.usage.credits?.cost;
|
|
134
|
+
return {
|
|
135
|
+
object: "list",
|
|
136
|
+
data: result.embeddings.map(item => ({ object: "embedding", ...item })),
|
|
137
|
+
model: requestedModelId,
|
|
138
|
+
usage: {
|
|
139
|
+
prompt_tokens: result.usage.input,
|
|
140
|
+
total_tokens: result.usage.totalTokens,
|
|
141
|
+
...(billedCost !== undefined && { cost: billedCost }),
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function formatError(status: number, errorType: string, message: string): Response {
|
|
147
|
+
return new Response(JSON.stringify({ error: { code: status, type: errorType, message } }), {
|
|
148
|
+
status,
|
|
149
|
+
headers: { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" },
|
|
150
|
+
});
|
|
151
|
+
}
|