@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.
Files changed (93) hide show
  1. package/CHANGELOG.md +16 -0
  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/embeddings/index.d.ts +7 -0
  14. package/dist/types/embeddings/openai-embeddings.d.ts +15 -0
  15. package/dist/types/embeddings/types.d.ts +15 -0
  16. package/dist/types/images/google-antigravity.d.ts +9 -0
  17. package/dist/types/images/google-generative-ai.d.ts +3 -0
  18. package/dist/types/images/index.d.ts +14 -0
  19. package/dist/types/images/openai-hosted.d.ts +3 -0
  20. package/dist/types/images/openai-images.d.ts +5 -0
  21. package/dist/types/images/openrouter-images.d.ts +3 -0
  22. package/dist/types/images/shared.d.ts +34 -0
  23. package/dist/types/images/types.d.ts +31 -0
  24. package/dist/types/index.d.ts +7 -1
  25. package/dist/types/judgment/typesafe.d.ts +2 -0
  26. package/dist/types/providers/embeddings-server.d.ts +32 -0
  27. package/dist/types/providers/images-server.d.ts +22 -0
  28. package/dist/types/providers/rerank-server.d.ts +35 -0
  29. package/dist/types/providers/speech-server.d.ts +8 -0
  30. package/dist/types/providers/systemone-server.d.ts +26 -0
  31. package/dist/types/providers/transcriptions-server.d.ts +32 -0
  32. package/dist/types/providers/video-server.d.ts +39 -0
  33. package/dist/types/rerank/index.d.ts +7 -0
  34. package/dist/types/rerank/openrouter-rerank.d.ts +15 -0
  35. package/dist/types/rerank/types.d.ts +17 -0
  36. package/dist/types/speech/index.d.ts +13 -0
  37. package/dist/types/speech/openai-speech.d.ts +3 -0
  38. package/dist/types/speech/transport.d.ts +7 -0
  39. package/dist/types/speech/types.d.ts +24 -0
  40. package/dist/types/speech/xai-tts.d.ts +7 -0
  41. package/dist/types/transcription/index.d.ts +7 -0
  42. package/dist/types/transcription/openai-transcriptions.d.ts +15 -0
  43. package/dist/types/transcription/types.d.ts +41 -0
  44. package/dist/types/video/index.d.ts +11 -0
  45. package/dist/types/video/openrouter-video.d.ts +19 -0
  46. package/dist/types/video/types.d.ts +62 -0
  47. package/package.json +30 -6
  48. package/src/auth-gateway/dispatch.ts +273 -0
  49. package/src/auth-gateway/http.ts +6 -7
  50. package/src/auth-gateway/index.ts +1 -0
  51. package/src/auth-gateway/routes/embeddings.ts +98 -0
  52. package/src/auth-gateway/routes/images.ts +131 -0
  53. package/src/auth-gateway/routes/rerank.ts +87 -0
  54. package/src/auth-gateway/routes/speech.ts +101 -0
  55. package/src/auth-gateway/routes/systemone.ts +116 -0
  56. package/src/auth-gateway/routes/transcriptions.ts +98 -0
  57. package/src/auth-gateway/routes/video.ts +243 -0
  58. package/src/auth-gateway/server.ts +123 -260
  59. package/src/embeddings/index.ts +17 -0
  60. package/src/embeddings/openai-embeddings.ts +141 -0
  61. package/src/embeddings/types.ts +14 -0
  62. package/src/error/rate-limit.ts +1 -1
  63. package/src/images/google-antigravity.ts +180 -0
  64. package/src/images/google-generative-ai.ts +92 -0
  65. package/src/images/index.ts +59 -0
  66. package/src/images/openai-hosted.ts +185 -0
  67. package/src/images/openai-images.ts +110 -0
  68. package/src/images/openrouter-images.ts +33 -0
  69. package/src/images/shared.ts +193 -0
  70. package/src/images/types.ts +36 -0
  71. package/src/index.ts +7 -1
  72. package/src/judgment/typesafe.ts +5 -0
  73. package/src/providers/embeddings-server.ts +151 -0
  74. package/src/providers/images-server.ts +159 -0
  75. package/src/providers/rerank-server.ts +166 -0
  76. package/src/providers/speech-server.ts +53 -0
  77. package/src/providers/systemone-server.ts +73 -0
  78. package/src/providers/transcriptions-server.ts +243 -0
  79. package/src/providers/video-server.ts +286 -0
  80. package/src/rerank/index.ts +13 -0
  81. package/src/rerank/openrouter-rerank.ts +136 -0
  82. package/src/rerank/types.ts +20 -0
  83. package/src/speech/index.ts +35 -0
  84. package/src/speech/openai-speech.ts +26 -0
  85. package/src/speech/transport.ts +66 -0
  86. package/src/speech/types.ts +37 -0
  87. package/src/speech/xai-tts.ts +41 -0
  88. package/src/transcription/index.ts +17 -0
  89. package/src/transcription/openai-transcriptions.ts +133 -0
  90. package/src/transcription/types.ts +46 -0
  91. package/src/video/index.ts +34 -0
  92. package/src/video/openrouter-video.ts +210 -0
  93. package/src/video/types.ts +72 -0
@@ -0,0 +1,133 @@
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 { TranscriptionRequest, TranscriptionResult, TranscriptionSegment, TranscriptionWord } from "./types";
7
+
8
+ export interface TranscriptionOptions {
9
+ apiKey: ApiKey;
10
+ fetch?: FetchImpl;
11
+ signal?: AbortSignal;
12
+ }
13
+
14
+ /** Non-2xx response from an OpenAI-compatible transcription endpoint. */
15
+ export class TranscriptionApiError extends AIError.ProviderHttpError {
16
+ override readonly name = "TranscriptionApiError";
17
+ }
18
+
19
+ const upstreamResponseSchema = type({
20
+ text: "string",
21
+ "language?": "string",
22
+ "duration?": "number",
23
+ "segments?": "object[]",
24
+ "words?": "object[]",
25
+ "usage?": "object",
26
+ });
27
+
28
+ interface UpstreamUsage {
29
+ input_tokens?: unknown;
30
+ output_tokens?: unknown;
31
+ total_tokens?: unknown;
32
+ cost?: unknown;
33
+ seconds?: unknown;
34
+ }
35
+
36
+ function finiteNumber(value: unknown): number | undefined {
37
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
38
+ }
39
+
40
+ function decodeUsage(model: Model<Api>, raw: unknown): { usage: Usage; seconds?: number } {
41
+ const upstream = raw && typeof raw === "object" ? (raw as UpstreamUsage) : {};
42
+ const input = finiteNumber(upstream.input_tokens) ?? 0;
43
+ const output = finiteNumber(upstream.output_tokens) ?? 0;
44
+ const reportedTotal = finiteNumber(upstream.total_tokens);
45
+ const reportedCost = finiteNumber(upstream.cost);
46
+ const usage: Usage = {
47
+ input,
48
+ output,
49
+ cacheRead: 0,
50
+ cacheWrite: 0,
51
+ totalTokens: reportedTotal ?? input + output,
52
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: reportedCost ?? 0 },
53
+ };
54
+ if (reportedCost === undefined) calculateCost(model, usage);
55
+ return { usage, seconds: finiteNumber(upstream.seconds) };
56
+ }
57
+
58
+ async function responseError(response: Response, model: Model<Api>): Promise<TranscriptionApiError> {
59
+ const text = await response.text();
60
+ let detail = text;
61
+ let code: string | undefined;
62
+ try {
63
+ const parsed: unknown = JSON.parse(text);
64
+ if (parsed && typeof parsed === "object" && "error" in parsed) {
65
+ const error = parsed.error;
66
+ if (error && typeof error === "object") {
67
+ const envelope = error as { message?: unknown; code?: unknown; type?: unknown };
68
+ if (typeof envelope.message === "string") detail = envelope.message;
69
+ if (typeof envelope.code === "string") code = envelope.code;
70
+ else if (typeof envelope.type === "string") code = envelope.type;
71
+ }
72
+ }
73
+ } catch {}
74
+ return new TranscriptionApiError(
75
+ `${model.provider}/${model.id} transcription API error (${response.status}): ${detail || response.statusText}`,
76
+ response.status,
77
+ { headers: response.headers, code },
78
+ );
79
+ }
80
+
81
+ /** Call an OpenAI/OpenRouter-compatible multipart transcription endpoint. */
82
+ export async function transcribeOpenAI(
83
+ model: Model<Api>,
84
+ request: TranscriptionRequest,
85
+ options: TranscriptionOptions,
86
+ ): Promise<TranscriptionResult> {
87
+ const form = new FormData();
88
+ const fileName = request.fileName?.trim() || "audio";
89
+ form.append("file", new File([request.audio], fileName, { type: request.mimeType }));
90
+ form.append("model", model.id);
91
+ form.append("response_format", request.responseFormat);
92
+ if (request.language !== undefined) form.append("language", request.language);
93
+ if (request.prompt !== undefined) form.append("prompt", request.prompt);
94
+ if (request.temperature !== undefined) form.append("temperature", String(request.temperature));
95
+ for (const granularity of request.timestampGranularities ?? []) {
96
+ form.append("timestamp_granularities[]", granularity);
97
+ }
98
+
99
+ const fetchImpl = options.fetch ?? fetch;
100
+ const response = await withAuth(
101
+ options.apiKey,
102
+ async key => {
103
+ const attempt = await fetchImpl(`${model.baseUrl.replace(/\/+$/, "")}/audio/transcriptions`, {
104
+ method: "POST",
105
+ headers: { Authorization: `Bearer ${key}`, Accept: "application/json" },
106
+ body: form,
107
+ signal: options.signal,
108
+ });
109
+ if (!attempt.ok) throw await responseError(attempt, model);
110
+ return attempt;
111
+ },
112
+ { signal: options.signal },
113
+ );
114
+
115
+ const body: unknown = await response.json();
116
+ const parsed = upstreamResponseSchema(body);
117
+ if (parsed instanceof type.errors) {
118
+ throw new AIError.ProviderResponseError(
119
+ `${model.provider}/${model.id} transcription response is malformed: ${parsed.summary}`,
120
+ { provider: model.provider, kind: "envelope" },
121
+ );
122
+ }
123
+ const decoded = decodeUsage(model, parsed.usage);
124
+ return {
125
+ text: parsed.text,
126
+ ...(parsed.language !== undefined && { language: parsed.language }),
127
+ ...(parsed.duration !== undefined && { duration: parsed.duration }),
128
+ ...(parsed.segments !== undefined && { segments: parsed.segments as TranscriptionSegment[] }),
129
+ ...(parsed.words !== undefined && { words: parsed.words as TranscriptionWord[] }),
130
+ ...(decoded.seconds !== undefined && { seconds: decoded.seconds }),
131
+ usage: decoded.usage,
132
+ };
133
+ }
@@ -0,0 +1,46 @@
1
+ import type { Usage } from "@oh-my-pi/pi-catalog/types";
2
+
3
+ export type TranscriptionResponseFormat = "json" | "verbose_json";
4
+ export type TranscriptionTimestampGranularity = "word" | "segment";
5
+
6
+ export interface TranscriptionRequest {
7
+ audio: Uint8Array;
8
+ mimeType: string;
9
+ fileName?: string;
10
+ language?: string;
11
+ prompt?: string;
12
+ temperature?: number;
13
+ responseFormat: TranscriptionResponseFormat;
14
+ timestampGranularities?: TranscriptionTimestampGranularity[];
15
+ }
16
+
17
+ /** Provider-specific segment fields are preserved alongside the normalized timestamps and text. */
18
+ export interface TranscriptionSegment {
19
+ id?: number | string;
20
+ start: number;
21
+ end: number;
22
+ text: string;
23
+ speaker?: number | string;
24
+ [key: string]: unknown;
25
+ }
26
+
27
+ /** Provider-specific word fields are preserved alongside the normalized timestamps and spelling. */
28
+ export interface TranscriptionWord {
29
+ word: string;
30
+ start: number;
31
+ end: number;
32
+ speaker?: number | string;
33
+ confidence?: number;
34
+ [key: string]: unknown;
35
+ }
36
+
37
+ export interface TranscriptionResult {
38
+ text: string;
39
+ language?: string;
40
+ duration?: number;
41
+ segments?: TranscriptionSegment[];
42
+ words?: TranscriptionWord[];
43
+ /** Provider-billed audio duration, which may be present without verbose output. */
44
+ seconds?: number;
45
+ usage: Usage;
46
+ }
@@ -0,0 +1,34 @@
1
+ import type { Api, Model } from "@oh-my-pi/pi-catalog/types";
2
+ import * as AIError from "../error";
3
+ import {
4
+ downloadOpenRouterVideo,
5
+ pollOpenRouterVideo,
6
+ submitOpenRouterVideo,
7
+ type VideoOptions,
8
+ } from "./openrouter-video";
9
+ import type { VideoContent, VideoGenerationRequest, VideoJob } from "./types";
10
+
11
+ export * from "./openrouter-video";
12
+ export * from "./types";
13
+
14
+ /** Submit an asynchronous video generation job through the model's transport. */
15
+ export function submitVideo(
16
+ model: Model<Api>,
17
+ request: VideoGenerationRequest,
18
+ options: VideoOptions,
19
+ ): Promise<VideoJob> {
20
+ if (model.api === "openrouter-video") return submitOpenRouterVideo(model, request, options);
21
+ throw new AIError.ConfigurationError(`Unsupported video API: ${model.api}`);
22
+ }
23
+
24
+ /** Poll an asynchronous video generation job through the model's transport. */
25
+ export function pollVideo(model: Model<Api>, jobId: string, options: VideoOptions): Promise<VideoJob> {
26
+ if (model.api === "openrouter-video") return pollOpenRouterVideo(model, jobId, options);
27
+ throw new AIError.ConfigurationError(`Unsupported video API: ${model.api}`);
28
+ }
29
+
30
+ /** Stream generated video content through the model's transport. */
31
+ export function downloadVideo(model: Model<Api>, jobId: string, options: VideoOptions): Promise<VideoContent> {
32
+ if (model.api === "openrouter-video") return downloadOpenRouterVideo(model, jobId, options);
33
+ throw new AIError.ConfigurationError(`Unsupported video API: ${model.api}`);
34
+ }
@@ -0,0 +1,210 @@
1
+ import type { Api, FetchImpl, Model, Usage } from "@oh-my-pi/pi-catalog/types";
2
+ import { type } from "@oh-my-pi/omptype";
3
+ import { type ApiKey, withAuth } from "../auth-retry";
4
+ import * as AIError from "../error";
5
+ import type {
6
+ VideoAudioReference,
7
+ VideoContent,
8
+ VideoFrameImage,
9
+ VideoGenerationRequest,
10
+ VideoImageReference,
11
+ VideoInputReference,
12
+ VideoJob,
13
+ VideoVideoReference,
14
+ } from "./types";
15
+
16
+ export interface VideoOptions {
17
+ apiKey: ApiKey;
18
+ fetch?: FetchImpl;
19
+ signal?: AbortSignal;
20
+ }
21
+
22
+ /** Non-2xx response from an OpenRouter video endpoint. */
23
+ export class VideoApiError extends AIError.ProviderHttpError {
24
+ override readonly name = "VideoApiError";
25
+ }
26
+
27
+ const videoJobSchema = type({
28
+ id: "string > 0",
29
+ status: "'queued' | 'processing' | 'pending' | 'in_progress' | 'completed' | 'failed' | 'cancelled' | 'expired'",
30
+ "polling_url?": "string",
31
+ "generation_id?": "string",
32
+ "unsigned_urls?": "string[]",
33
+ "error?": "string",
34
+ "usage?": "object",
35
+ });
36
+
37
+ function videoBaseUrl(model: Model<Api>): string {
38
+ return model.baseUrl.replace(/\/+$/, "");
39
+ }
40
+
41
+ async function responseError(response: Response, model: Model<Api>): Promise<VideoApiError> {
42
+ const text = await response.text();
43
+ let detail = text;
44
+ let code: string | undefined;
45
+ try {
46
+ const parsed: unknown = JSON.parse(text);
47
+ if (parsed && typeof parsed === "object" && "error" in parsed) {
48
+ const error = parsed.error;
49
+ if (typeof error === "string") detail = error;
50
+ else if (error && typeof error === "object") {
51
+ const envelope = error as { message?: unknown; code?: unknown; type?: unknown };
52
+ if (typeof envelope.message === "string") detail = envelope.message;
53
+ if (typeof envelope.code === "string") code = envelope.code;
54
+ else if (typeof envelope.type === "string") code = envelope.type;
55
+ }
56
+ }
57
+ } catch {}
58
+ return new VideoApiError(
59
+ `${model.provider}/${model.id} video API error (${response.status}): ${detail || response.statusText}`,
60
+ response.status,
61
+ { headers: response.headers, code },
62
+ );
63
+ }
64
+
65
+ function imageReference(reference: VideoImageReference): Record<string, unknown> {
66
+ return { type: reference.type, image_url: reference.imageUrl };
67
+ }
68
+
69
+ function frameImage(reference: VideoFrameImage): Record<string, unknown> {
70
+ return { ...imageReference(reference), frame_type: reference.frameType };
71
+ }
72
+
73
+ function inputReference(reference: VideoInputReference): Record<string, unknown> {
74
+ switch (reference.type) {
75
+ case "image_url":
76
+ return imageReference(reference);
77
+ case "audio_url":
78
+ return { type: reference.type, audio_url: (reference as VideoAudioReference).audioUrl };
79
+ case "video_url":
80
+ return { type: reference.type, video_url: (reference as VideoVideoReference).videoUrl };
81
+ }
82
+ }
83
+
84
+ function encodeRequest(model: Model<Api>, request: VideoGenerationRequest): Record<string, unknown> {
85
+ return {
86
+ model: model.requestModelId ?? model.id,
87
+ ...(request.prompt !== undefined && { prompt: request.prompt }),
88
+ ...(request.duration !== undefined && { duration: request.duration }),
89
+ ...(request.resolution !== undefined && { resolution: request.resolution }),
90
+ ...(request.aspectRatio !== undefined && { aspect_ratio: request.aspectRatio }),
91
+ ...(request.size !== undefined && { size: request.size }),
92
+ ...(request.frameImages !== undefined && { frame_images: request.frameImages.map(frameImage) }),
93
+ ...(request.inputReferences !== undefined && {
94
+ input_references: request.inputReferences.map(inputReference),
95
+ }),
96
+ ...(request.generateAudio !== undefined && { generate_audio: request.generateAudio }),
97
+ ...(request.seed !== undefined && { seed: request.seed }),
98
+ ...(request.callbackUrl !== undefined && { callback_url: request.callbackUrl }),
99
+ ...(request.provider !== undefined && { provider: request.provider }),
100
+ ...(request.previousJobId !== undefined && { previous_job_id: request.previousJobId }),
101
+ ...(request.sessionId !== undefined && { session_id: request.sessionId }),
102
+ ...(request.trace !== undefined && { trace: request.trace }),
103
+ ...(request.user !== undefined && { user: request.user }),
104
+ ...(request.creativity !== undefined && { creativity: request.creativity }),
105
+ ...(request.upscaleFactor !== undefined && { upscale_factor: request.upscaleFactor }),
106
+ };
107
+ }
108
+
109
+ function decodeUsage(raw: unknown): Usage | undefined {
110
+ if (!raw || typeof raw !== "object") return undefined;
111
+ const cost = "cost" in raw && typeof raw.cost === "number" && Number.isFinite(raw.cost) ? raw.cost : undefined;
112
+ if (cost === undefined) return undefined;
113
+ return {
114
+ input: 0,
115
+ output: 0,
116
+ cacheRead: 0,
117
+ cacheWrite: 0,
118
+ totalTokens: 0,
119
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: cost },
120
+ };
121
+ }
122
+
123
+ async function decodeJob(response: Response, model: Model<Api>): Promise<VideoJob> {
124
+ const body: unknown = await response.json();
125
+ const parsed = videoJobSchema(body);
126
+ if (parsed instanceof type.errors) {
127
+ throw new AIError.ProviderResponseError(
128
+ `${model.provider}/${model.id} video response is malformed: ${parsed.summary}`,
129
+ { provider: model.provider, kind: "envelope" },
130
+ );
131
+ }
132
+ const usage = decodeUsage(parsed.usage);
133
+ return {
134
+ id: parsed.id,
135
+ status: parsed.status,
136
+ ...(parsed.polling_url !== undefined && { pollingUrl: parsed.polling_url }),
137
+ ...(parsed.generation_id !== undefined && { generationId: parsed.generation_id }),
138
+ ...(parsed.unsigned_urls !== undefined && { contentUrls: parsed.unsigned_urls }),
139
+ ...(parsed.error !== undefined && { error: parsed.error }),
140
+ ...(usage !== undefined && { usage }),
141
+ };
142
+ }
143
+
144
+ async function authenticatedFetch(
145
+ model: Model<Api>,
146
+ path: string,
147
+ options: VideoOptions,
148
+ init?: Omit<RequestInit, "signal">,
149
+ ): Promise<Response> {
150
+ const fetchImpl = options.fetch ?? fetch;
151
+ return withAuth(
152
+ options.apiKey,
153
+ async key => {
154
+ const response = await fetchImpl(`${videoBaseUrl(model)}${path}`, {
155
+ ...init,
156
+ headers: { Authorization: `Bearer ${key}`, Accept: "application/json", ...init?.headers },
157
+ signal: options.signal,
158
+ });
159
+ if (!response.ok) throw await responseError(response, model);
160
+ return response;
161
+ },
162
+ { signal: options.signal },
163
+ );
164
+ }
165
+
166
+ /** Submit an asynchronous OpenRouter video generation job. */
167
+ export async function submitOpenRouterVideo(
168
+ model: Model<Api>,
169
+ request: VideoGenerationRequest,
170
+ options: VideoOptions,
171
+ ): Promise<VideoJob> {
172
+ const response = await authenticatedFetch(model, "/videos", options, {
173
+ method: "POST",
174
+ headers: { "Content-Type": "application/json" },
175
+ body: JSON.stringify(encodeRequest(model, request)),
176
+ });
177
+ return decodeJob(response, model);
178
+ }
179
+
180
+ /** Poll an asynchronous OpenRouter video generation job. */
181
+ export async function pollOpenRouterVideo(model: Model<Api>, jobId: string, options: VideoOptions): Promise<VideoJob> {
182
+ const response = await authenticatedFetch(model, `/videos/${encodeURIComponent(jobId)}`, options);
183
+ return decodeJob(response, model);
184
+ }
185
+
186
+ /** Stream generated video bytes from OpenRouter without buffering them in memory. */
187
+ export async function downloadOpenRouterVideo(
188
+ model: Model<Api>,
189
+ jobId: string,
190
+ options: VideoOptions,
191
+ ): Promise<VideoContent> {
192
+ const response = await authenticatedFetch(model, `/videos/${encodeURIComponent(jobId)}/content`, options, {
193
+ headers: { Accept: "video/*" },
194
+ });
195
+ if (!response.body) {
196
+ throw new AIError.ProviderResponseError(`${model.provider}/${model.id} video content response has no body`, {
197
+ provider: model.provider,
198
+ kind: "envelope",
199
+ });
200
+ }
201
+ const contentLengthHeader = response.headers.get("content-length");
202
+ const contentLength = contentLengthHeader === null ? undefined : Number(contentLengthHeader);
203
+ return {
204
+ body: response.body,
205
+ contentType: response.headers.get("content-type") ?? "application/octet-stream",
206
+ ...(contentLength !== undefined && Number.isSafeInteger(contentLength) && contentLength >= 0
207
+ ? { contentLength }
208
+ : {}),
209
+ };
210
+ }
@@ -0,0 +1,72 @@
1
+ import type { Usage } from "@oh-my-pi/pi-catalog/types";
2
+
3
+ export type VideoResolution = "360p" | "480p" | "720p" | "768p" | "1080p" | "1K" | "2K" | "4K";
4
+ export type VideoAspectRatio = "16:9" | "9:16" | "1:1" | "4:3" | "3:4" | "3:2" | "2:3" | "21:9" | "9:21";
5
+
6
+ export interface VideoImageReference {
7
+ type: "image_url";
8
+ imageUrl: { url: string };
9
+ }
10
+
11
+ export interface VideoFrameImage extends VideoImageReference {
12
+ frameType: "first_frame" | "last_frame";
13
+ }
14
+
15
+ export interface VideoAudioReference {
16
+ type: "audio_url";
17
+ audioUrl: { url: string };
18
+ }
19
+
20
+ export interface VideoVideoReference {
21
+ type: "video_url";
22
+ videoUrl: { url: string };
23
+ }
24
+
25
+ export type VideoInputReference = VideoImageReference | VideoAudioReference | VideoVideoReference;
26
+
27
+ /** Canonical camel-case form of OpenRouter's asynchronous video submit request. */
28
+ export interface VideoGenerationRequest {
29
+ prompt?: string;
30
+ duration?: number;
31
+ resolution?: VideoResolution;
32
+ aspectRatio?: VideoAspectRatio;
33
+ size?: string;
34
+ frameImages?: VideoFrameImage[];
35
+ inputReferences?: VideoInputReference[];
36
+ generateAudio?: boolean;
37
+ seed?: number;
38
+ callbackUrl?: string;
39
+ provider?: { options?: Record<string, unknown> };
40
+ previousJobId?: string;
41
+ sessionId?: string;
42
+ trace?: Record<string, unknown>;
43
+ user?: string;
44
+ creativity?: number;
45
+ upscaleFactor?: number;
46
+ }
47
+
48
+ export type VideoJobStatus =
49
+ | "queued"
50
+ | "processing"
51
+ | "pending"
52
+ | "in_progress"
53
+ | "completed"
54
+ | "failed"
55
+ | "cancelled"
56
+ | "expired";
57
+
58
+ export interface VideoJob {
59
+ id: string;
60
+ status: VideoJobStatus;
61
+ pollingUrl?: string;
62
+ generationId?: string;
63
+ contentUrls?: string[];
64
+ error?: string;
65
+ usage?: Usage;
66
+ }
67
+
68
+ export interface VideoContent {
69
+ body: ReadableStream<Uint8Array>;
70
+ contentType: string;
71
+ contentLength?: number;
72
+ }