@nodetool-ai/runtime 0.7.0-rc.11
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/dist/bridge-protocol.d.ts +39 -0
- package/dist/bridge-protocol.d.ts.map +1 -0
- package/dist/bridge-protocol.js +39 -0
- package/dist/bridge-protocol.js.map +1 -0
- package/dist/comfy-executor.d.ts +41 -0
- package/dist/comfy-executor.d.ts.map +1 -0
- package/dist/comfy-executor.js +305 -0
- package/dist/comfy-executor.js.map +1 -0
- package/dist/context-packer.d.ts +22 -0
- package/dist/context-packer.d.ts.map +1 -0
- package/dist/context-packer.js +66 -0
- package/dist/context-packer.js.map +1 -0
- package/dist/context.d.ts +405 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +1455 -0
- package/dist/context.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/node-executor.d.ts +53 -0
- package/dist/node-executor.d.ts.map +1 -0
- package/dist/node-executor.js +9 -0
- package/dist/node-executor.js.map +1 -0
- package/dist/providers/aki-manifest.json +34 -0
- package/dist/providers/aki-provider.d.ts +65 -0
- package/dist/providers/aki-provider.d.ts.map +1 -0
- package/dist/providers/aki-provider.js +517 -0
- package/dist/providers/aki-provider.js.map +1 -0
- package/dist/providers/anthropic-provider.d.ts +55 -0
- package/dist/providers/anthropic-provider.d.ts.map +1 -0
- package/dist/providers/anthropic-provider.js +563 -0
- package/dist/providers/anthropic-provider.js.map +1 -0
- package/dist/providers/base-provider.d.ts +194 -0
- package/dist/providers/base-provider.d.ts.map +1 -0
- package/dist/providers/base-provider.js +515 -0
- package/dist/providers/base-provider.js.map +1 -0
- package/dist/providers/cerebras-provider.d.ts +20 -0
- package/dist/providers/cerebras-provider.d.ts.map +1 -0
- package/dist/providers/cerebras-provider.js +52 -0
- package/dist/providers/cerebras-provider.js.map +1 -0
- package/dist/providers/claude-agent-provider.d.ts +107 -0
- package/dist/providers/claude-agent-provider.d.ts.map +1 -0
- package/dist/providers/claude-agent-provider.js +586 -0
- package/dist/providers/claude-agent-provider.js.map +1 -0
- package/dist/providers/cohere-provider.d.ts +36 -0
- package/dist/providers/cohere-provider.d.ts.map +1 -0
- package/dist/providers/cohere-provider.js +116 -0
- package/dist/providers/cohere-provider.js.map +1 -0
- package/dist/providers/cost-calculator.d.ts +109 -0
- package/dist/providers/cost-calculator.d.ts.map +1 -0
- package/dist/providers/cost-calculator.js +475 -0
- package/dist/providers/cost-calculator.js.map +1 -0
- package/dist/providers/deepseek-provider.d.ts +25 -0
- package/dist/providers/deepseek-provider.d.ts.map +1 -0
- package/dist/providers/deepseek-provider.js +58 -0
- package/dist/providers/deepseek-provider.js.map +1 -0
- package/dist/providers/fake-provider.d.ts +52 -0
- package/dist/providers/fake-provider.d.ts.map +1 -0
- package/dist/providers/fake-provider.js +107 -0
- package/dist/providers/fake-provider.js.map +1 -0
- package/dist/providers/fal-provider.d.ts +32 -0
- package/dist/providers/fal-provider.d.ts.map +1 -0
- package/dist/providers/fal-provider.js +445 -0
- package/dist/providers/fal-provider.js.map +1 -0
- package/dist/providers/gemini-provider.d.ts +109 -0
- package/dist/providers/gemini-provider.d.ts.map +1 -0
- package/dist/providers/gemini-provider.js +943 -0
- package/dist/providers/gemini-provider.js.map +1 -0
- package/dist/providers/groq-provider.d.ts +20 -0
- package/dist/providers/groq-provider.d.ts.map +1 -0
- package/dist/providers/groq-provider.js +52 -0
- package/dist/providers/groq-provider.js.map +1 -0
- package/dist/providers/huggingface-provider.d.ts +59 -0
- package/dist/providers/huggingface-provider.d.ts.map +1 -0
- package/dist/providers/huggingface-provider.js +305 -0
- package/dist/providers/huggingface-provider.js.map +1 -0
- package/dist/providers/index.d.ts +69 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +137 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/jina-provider.d.ts +37 -0
- package/dist/providers/jina-provider.d.ts.map +1 -0
- package/dist/providers/jina-provider.js +118 -0
- package/dist/providers/jina-provider.js.map +1 -0
- package/dist/providers/kie-provider.d.ts +19 -0
- package/dist/providers/kie-provider.d.ts.map +1 -0
- package/dist/providers/kie-provider.js +118 -0
- package/dist/providers/kie-provider.js.map +1 -0
- package/dist/providers/llama-provider.d.ts +49 -0
- package/dist/providers/llama-provider.d.ts.map +1 -0
- package/dist/providers/llama-provider.js +381 -0
- package/dist/providers/llama-provider.js.map +1 -0
- package/dist/providers/lmstudio-provider.d.ts +22 -0
- package/dist/providers/lmstudio-provider.d.ts.map +1 -0
- package/dist/providers/lmstudio-provider.js +63 -0
- package/dist/providers/lmstudio-provider.js.map +1 -0
- package/dist/providers/manifest-models.d.ts +12 -0
- package/dist/providers/manifest-models.d.ts.map +1 -0
- package/dist/providers/manifest-models.js +96 -0
- package/dist/providers/manifest-models.js.map +1 -0
- package/dist/providers/meshy-provider.d.ts +53 -0
- package/dist/providers/meshy-provider.d.ts.map +1 -0
- package/dist/providers/meshy-provider.js +230 -0
- package/dist/providers/meshy-provider.js.map +1 -0
- package/dist/providers/minimax-provider.d.ts +66 -0
- package/dist/providers/minimax-provider.d.ts.map +1 -0
- package/dist/providers/minimax-provider.js +531 -0
- package/dist/providers/minimax-provider.js.map +1 -0
- package/dist/providers/mistral-provider.d.ts +26 -0
- package/dist/providers/mistral-provider.d.ts.map +1 -0
- package/dist/providers/mistral-provider.js +74 -0
- package/dist/providers/mistral-provider.js.map +1 -0
- package/dist/providers/moonshot-provider.d.ts +24 -0
- package/dist/providers/moonshot-provider.d.ts.map +1 -0
- package/dist/providers/moonshot-provider.js +43 -0
- package/dist/providers/moonshot-provider.js.map +1 -0
- package/dist/providers/ollama-provider.d.ts +71 -0
- package/dist/providers/ollama-provider.d.ts.map +1 -0
- package/dist/providers/ollama-provider.js +480 -0
- package/dist/providers/ollama-provider.js.map +1 -0
- package/dist/providers/openai-provider.d.ts +103 -0
- package/dist/providers/openai-provider.d.ts.map +1 -0
- package/dist/providers/openai-provider.js +917 -0
- package/dist/providers/openai-provider.js.map +1 -0
- package/dist/providers/openrouter-provider.d.ts +49 -0
- package/dist/providers/openrouter-provider.d.ts.map +1 -0
- package/dist/providers/openrouter-provider.js +147 -0
- package/dist/providers/openrouter-provider.js.map +1 -0
- package/dist/providers/provider-registry.d.ts +24 -0
- package/dist/providers/provider-registry.d.ts.map +1 -0
- package/dist/providers/provider-registry.js +124 -0
- package/dist/providers/provider-registry.js.map +1 -0
- package/dist/providers/python-provider.d.ts +75 -0
- package/dist/providers/python-provider.d.ts.map +1 -0
- package/dist/providers/python-provider.js +157 -0
- package/dist/providers/python-provider.js.map +1 -0
- package/dist/providers/replicate-provider.d.ts +73 -0
- package/dist/providers/replicate-provider.d.ts.map +1 -0
- package/dist/providers/replicate-provider.js +484 -0
- package/dist/providers/replicate-provider.js.map +1 -0
- package/dist/providers/rodin-provider.d.ts +41 -0
- package/dist/providers/rodin-provider.d.ts.map +1 -0
- package/dist/providers/rodin-provider.js +236 -0
- package/dist/providers/rodin-provider.js.map +1 -0
- package/dist/providers/scripted-provider.d.ts +141 -0
- package/dist/providers/scripted-provider.d.ts.map +1 -0
- package/dist/providers/scripted-provider.js +246 -0
- package/dist/providers/scripted-provider.js.map +1 -0
- package/dist/providers/together-provider.d.ts +20 -0
- package/dist/providers/together-provider.d.ts.map +1 -0
- package/dist/providers/together-provider.js +56 -0
- package/dist/providers/together-provider.js.map +1 -0
- package/dist/providers/types.d.ts +202 -0
- package/dist/providers/types.d.ts.map +1 -0
- package/dist/providers/types.js +2 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/providers/vllm-provider.d.ts +22 -0
- package/dist/providers/vllm-provider.d.ts.map +1 -0
- package/dist/providers/vllm-provider.js +66 -0
- package/dist/providers/vllm-provider.js.map +1 -0
- package/dist/providers/voyage-provider.d.ts +37 -0
- package/dist/providers/voyage-provider.d.ts.map +1 -0
- package/dist/providers/voyage-provider.js +124 -0
- package/dist/providers/voyage-provider.js.map +1 -0
- package/dist/providers/xai-provider.d.ts +24 -0
- package/dist/providers/xai-provider.d.ts.map +1 -0
- package/dist/providers/xai-provider.js +57 -0
- package/dist/providers/xai-provider.js.map +1 -0
- package/dist/python-bridge-types.d.ts +70 -0
- package/dist/python-bridge-types.d.ts.map +1 -0
- package/dist/python-bridge-types.js +2 -0
- package/dist/python-bridge-types.js.map +1 -0
- package/dist/python-node-executor.d.ts +20 -0
- package/dist/python-node-executor.d.ts.map +1 -0
- package/dist/python-node-executor.js +219 -0
- package/dist/python-node-executor.js.map +1 -0
- package/dist/python-stdio-bridge.d.ts +67 -0
- package/dist/python-stdio-bridge.d.ts.map +1 -0
- package/dist/python-stdio-bridge.js +704 -0
- package/dist/python-stdio-bridge.js.map +1 -0
- package/dist/recommended-models.d.ts +17 -0
- package/dist/recommended-models.d.ts.map +1 -0
- package/dist/recommended-models.js +139 -0
- package/dist/recommended-models.js.map +1 -0
- package/dist/telemetry.d.ts +61 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +128 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/trace-exporters.d.ts +72 -0
- package/dist/trace-exporters.d.ts.map +1 -0
- package/dist/trace-exporters.js +216 -0
- package/dist/trace-exporters.js.map +1 -0
- package/dist/tracing-helpers.d.ts +99 -0
- package/dist/tracing-helpers.d.ts.map +1 -0
- package/dist/tracing-helpers.js +217 -0
- package/dist/tracing-helpers.js.map +1 -0
- package/package.json +64 -0
|
@@ -0,0 +1,917 @@
|
|
|
1
|
+
import OpenAI from "openai";
|
|
2
|
+
import { createLogger } from "@nodetool-ai/config";
|
|
3
|
+
import { BaseProvider } from "./base-provider.js";
|
|
4
|
+
const log = createLogger("nodetool.runtime.providers.openai");
|
|
5
|
+
function asUint8Array(data) {
|
|
6
|
+
if (data instanceof Uint8Array) {
|
|
7
|
+
return data;
|
|
8
|
+
}
|
|
9
|
+
if (Array.isArray(data) && data.every((v) => Number.isInteger(v))) {
|
|
10
|
+
return new Uint8Array(data);
|
|
11
|
+
}
|
|
12
|
+
if (typeof data === "string") {
|
|
13
|
+
return Uint8Array.from(Buffer.from(data, "base64"));
|
|
14
|
+
}
|
|
15
|
+
if (data instanceof ArrayBuffer) {
|
|
16
|
+
return new Uint8Array(data);
|
|
17
|
+
}
|
|
18
|
+
throw new Error("Unsupported binary payload format");
|
|
19
|
+
}
|
|
20
|
+
function toInt16Samples(bytes) {
|
|
21
|
+
const aligned = bytes.length % 2 === 0 ? bytes : bytes.slice(0, bytes.length - 1);
|
|
22
|
+
return new Int16Array(aligned.buffer, aligned.byteOffset, aligned.byteLength / 2);
|
|
23
|
+
}
|
|
24
|
+
function parseDataUri(uri) {
|
|
25
|
+
const idx = uri.indexOf(",");
|
|
26
|
+
if (idx < 0) {
|
|
27
|
+
throw new Error("Invalid data URI");
|
|
28
|
+
}
|
|
29
|
+
const header = uri.slice(5, idx);
|
|
30
|
+
const payload = uri.slice(idx + 1);
|
|
31
|
+
const isBase64 = header.includes(";base64");
|
|
32
|
+
const mime = header.split(";")[0] || "application/octet-stream";
|
|
33
|
+
if (isBase64) {
|
|
34
|
+
return { mime, data: Uint8Array.from(Buffer.from(payload, "base64")) };
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
mime,
|
|
38
|
+
data: Uint8Array.from(Buffer.from(decodeURIComponent(payload), "utf8"))
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function makeDataUri(mime, data) {
|
|
42
|
+
const b64 = Buffer.from(data).toString("base64");
|
|
43
|
+
return `data:${mime};base64,${b64}`;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Custom JSON replacer that handles objects with toJSON() methods
|
|
47
|
+
* and other non-serializable types. Mirrors Python's _default_serializer.
|
|
48
|
+
*/
|
|
49
|
+
function defaultSerializer(_key, value) {
|
|
50
|
+
if (value === null || value === undefined)
|
|
51
|
+
return value;
|
|
52
|
+
if (typeof value === "bigint")
|
|
53
|
+
return Number(value);
|
|
54
|
+
if (value instanceof Date)
|
|
55
|
+
return value.toISOString();
|
|
56
|
+
if (value instanceof Map)
|
|
57
|
+
return Object.fromEntries(value);
|
|
58
|
+
if (value instanceof Set)
|
|
59
|
+
return [...value];
|
|
60
|
+
if (typeof value === "object" &&
|
|
61
|
+
"toJSON" in value &&
|
|
62
|
+
typeof value.toJSON === "function") {
|
|
63
|
+
return value.toJSON();
|
|
64
|
+
}
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
export class OpenAIProvider extends BaseProvider {
|
|
68
|
+
static requiredSecrets() {
|
|
69
|
+
return ["OPENAI_API_KEY"];
|
|
70
|
+
}
|
|
71
|
+
apiKey;
|
|
72
|
+
_client;
|
|
73
|
+
_clientFactory;
|
|
74
|
+
_fetch;
|
|
75
|
+
constructor(secrets, options = {}) {
|
|
76
|
+
super("openai");
|
|
77
|
+
const apiKey = secrets.OPENAI_API_KEY;
|
|
78
|
+
if (!apiKey) {
|
|
79
|
+
throw new Error("OPENAI_API_KEY is required");
|
|
80
|
+
}
|
|
81
|
+
this.apiKey = apiKey;
|
|
82
|
+
this._client = options.client ?? null;
|
|
83
|
+
this._clientFactory =
|
|
84
|
+
options.clientFactory ?? ((key) => new OpenAI({ apiKey: key }));
|
|
85
|
+
this._fetch = options.fetchFn ?? globalThis.fetch.bind(globalThis);
|
|
86
|
+
}
|
|
87
|
+
getContainerEnv() {
|
|
88
|
+
return { OPENAI_API_KEY: this.apiKey };
|
|
89
|
+
}
|
|
90
|
+
getClient() {
|
|
91
|
+
if (!this._client) {
|
|
92
|
+
this._client = this._clientFactory(this.apiKey);
|
|
93
|
+
}
|
|
94
|
+
return this._client;
|
|
95
|
+
}
|
|
96
|
+
async hasToolSupport(model) {
|
|
97
|
+
return !(model.startsWith("o1") || model.startsWith("o3"));
|
|
98
|
+
}
|
|
99
|
+
async getAvailableLanguageModels() {
|
|
100
|
+
const response = await this._fetch("https://api.openai.com/v1/models", {
|
|
101
|
+
headers: {
|
|
102
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
const payload = (await response.json());
|
|
109
|
+
const rows = payload.data ?? [];
|
|
110
|
+
return rows
|
|
111
|
+
.filter((row) => typeof row.id === "string" && row.id.length > 0)
|
|
112
|
+
.map((row) => ({
|
|
113
|
+
id: row.id,
|
|
114
|
+
name: row.id,
|
|
115
|
+
provider: "openai"
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
async getAvailableTTSModels() {
|
|
119
|
+
return [
|
|
120
|
+
{
|
|
121
|
+
id: "tts-1",
|
|
122
|
+
name: "TTS 1",
|
|
123
|
+
provider: "openai",
|
|
124
|
+
voices: ["alloy", "echo", "fable", "onyx", "nova", "shimmer"]
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
id: "tts-1-hd",
|
|
128
|
+
name: "TTS 1 HD",
|
|
129
|
+
provider: "openai",
|
|
130
|
+
voices: ["alloy", "echo", "fable", "onyx", "nova", "shimmer"]
|
|
131
|
+
}
|
|
132
|
+
];
|
|
133
|
+
}
|
|
134
|
+
async getAvailableASRModels() {
|
|
135
|
+
return [
|
|
136
|
+
{
|
|
137
|
+
id: "whisper-1",
|
|
138
|
+
name: "Whisper",
|
|
139
|
+
provider: "openai"
|
|
140
|
+
}
|
|
141
|
+
];
|
|
142
|
+
}
|
|
143
|
+
async getAvailableVideoModels() {
|
|
144
|
+
return [
|
|
145
|
+
{
|
|
146
|
+
id: "sora-2",
|
|
147
|
+
name: "Sora 2",
|
|
148
|
+
provider: "openai",
|
|
149
|
+
supportedTasks: ["text_to_video"]
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
id: "sora-2-pro",
|
|
153
|
+
name: "Sora 2 Pro",
|
|
154
|
+
provider: "openai",
|
|
155
|
+
supportedTasks: ["text_to_video"]
|
|
156
|
+
}
|
|
157
|
+
];
|
|
158
|
+
}
|
|
159
|
+
async getAvailableImageModels() {
|
|
160
|
+
return [
|
|
161
|
+
{
|
|
162
|
+
id: "gpt-image-2",
|
|
163
|
+
name: "GPT Image 2",
|
|
164
|
+
provider: "openai",
|
|
165
|
+
supportedTasks: ["text_to_image", "image_to_image"]
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
id: "gpt-image-1.5",
|
|
169
|
+
name: "GPT Image 1.5",
|
|
170
|
+
provider: "openai",
|
|
171
|
+
supportedTasks: ["text_to_image", "image_to_image"]
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
id: "gpt-image-1",
|
|
175
|
+
name: "GPT Image 1",
|
|
176
|
+
provider: "openai",
|
|
177
|
+
supportedTasks: ["text_to_image", "image_to_image"]
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
id: "gpt-image-1-mini",
|
|
181
|
+
name: "GPT Image 1 Mini",
|
|
182
|
+
provider: "openai",
|
|
183
|
+
supportedTasks: ["text_to_image", "image_to_image"]
|
|
184
|
+
}
|
|
185
|
+
];
|
|
186
|
+
}
|
|
187
|
+
async getAvailableEmbeddingModels() {
|
|
188
|
+
return [
|
|
189
|
+
{
|
|
190
|
+
id: "text-embedding-3-small",
|
|
191
|
+
name: "Text Embedding 3 Small",
|
|
192
|
+
provider: "openai",
|
|
193
|
+
dimensions: 1536
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
id: "text-embedding-3-large",
|
|
197
|
+
name: "Text Embedding 3 Large",
|
|
198
|
+
provider: "openai",
|
|
199
|
+
dimensions: 3072
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
id: "text-embedding-ada-002",
|
|
203
|
+
name: "Text Embedding Ada 002",
|
|
204
|
+
provider: "openai",
|
|
205
|
+
dimensions: 1536
|
|
206
|
+
}
|
|
207
|
+
];
|
|
208
|
+
}
|
|
209
|
+
resolveImageSize(width, height) {
|
|
210
|
+
if (!width || !height) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
const supported = [
|
|
214
|
+
[1024, 1024],
|
|
215
|
+
[1024, 1536],
|
|
216
|
+
[1536, 1024]
|
|
217
|
+
];
|
|
218
|
+
const targetArea = width * height;
|
|
219
|
+
const targetRatio = width / height;
|
|
220
|
+
const score = (w, h) => {
|
|
221
|
+
const area = w * h;
|
|
222
|
+
const ratio = w / h;
|
|
223
|
+
const areaScore = Math.abs(area - targetArea) / Math.max(area, targetArea);
|
|
224
|
+
const ratioScore = Math.abs(ratio - targetRatio);
|
|
225
|
+
return areaScore * 0.7 + ratioScore * 0.3;
|
|
226
|
+
};
|
|
227
|
+
const best = supported.reduce((bestSoFar, candidate) => score(candidate[0], candidate[1]) < score(bestSoFar[0], bestSoFar[1])
|
|
228
|
+
? candidate
|
|
229
|
+
: bestSoFar);
|
|
230
|
+
return `${best[0]}x${best[1]}`;
|
|
231
|
+
}
|
|
232
|
+
static resolveVideoSize(aspectRatio, resolution) {
|
|
233
|
+
if (!resolution) {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
const supported = [
|
|
237
|
+
[1280, 720],
|
|
238
|
+
[1792, 1024],
|
|
239
|
+
[720, 1280],
|
|
240
|
+
[1024, 1792]
|
|
241
|
+
];
|
|
242
|
+
const aspect = (aspectRatio ?? "16:9").replaceAll(" ", "");
|
|
243
|
+
const resolutionKey = resolution.toLowerCase();
|
|
244
|
+
const presets = {
|
|
245
|
+
"16:9": { "480p": "854x480", "720p": "1280x720", "1080p": "1920x1080" },
|
|
246
|
+
"9:16": { "480p": "480x854", "720p": "720x1280", "1080p": "1080x1920" },
|
|
247
|
+
"1:1": { "480p": "480x480", "720p": "720x720", "1080p": "1080x1080" },
|
|
248
|
+
"4:3": { "480p": "640x480", "720p": "960x720", "1080p": "1440x1080" },
|
|
249
|
+
"3:4": { "480p": "480x640", "720p": "720x960", "1080p": "1080x1440" }
|
|
250
|
+
};
|
|
251
|
+
const score = (targetW, targetH, candidate) => {
|
|
252
|
+
const [w, h] = candidate;
|
|
253
|
+
const ratioDiff = Math.abs(w / h - targetW / targetH);
|
|
254
|
+
const areaDiff = Math.abs(w * h - targetW * targetH) /
|
|
255
|
+
Math.max(w * h, targetW * targetH);
|
|
256
|
+
return ratioDiff * 5 + areaDiff;
|
|
257
|
+
};
|
|
258
|
+
const closest = (targetW, targetH) => {
|
|
259
|
+
const best = supported.reduce((bestSoFar, candidate) => score(targetW, targetH, candidate) < score(targetW, targetH, bestSoFar)
|
|
260
|
+
? candidate
|
|
261
|
+
: bestSoFar);
|
|
262
|
+
return `${best[0]}x${best[1]}`;
|
|
263
|
+
};
|
|
264
|
+
const preset = presets[aspect]?.[resolutionKey];
|
|
265
|
+
if (preset) {
|
|
266
|
+
const [w, h] = preset.split("x").map(Number);
|
|
267
|
+
return closest(w, h);
|
|
268
|
+
}
|
|
269
|
+
const numeric = Number(resolutionKey.replace(/[^0-9]/g, ""));
|
|
270
|
+
if (!Number.isFinite(numeric) || numeric <= 0) {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
const [arW, arH] = aspect.includes(":")
|
|
274
|
+
? aspect.split(":").map((n) => Number(n))
|
|
275
|
+
: [1, 1];
|
|
276
|
+
if (!Number.isFinite(arW) ||
|
|
277
|
+
!Number.isFinite(arH) ||
|
|
278
|
+
arW <= 0 ||
|
|
279
|
+
arH <= 0) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
const targetW = arW >= arH ? numeric * (arW / arH) : numeric;
|
|
283
|
+
const targetH = arW >= arH ? numeric : numeric * (arH / arW);
|
|
284
|
+
return closest(Math.max(2, Math.round(targetW)), Math.max(2, Math.round(targetH)));
|
|
285
|
+
}
|
|
286
|
+
static secondsFromParams(params) {
|
|
287
|
+
const numFrames = params.numFrames;
|
|
288
|
+
if (!numFrames || numFrames <= 0) {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
const estimated = Math.ceil(numFrames / 24);
|
|
292
|
+
if (estimated < 8)
|
|
293
|
+
return 4;
|
|
294
|
+
if (estimated < 12)
|
|
295
|
+
return 8;
|
|
296
|
+
return 12;
|
|
297
|
+
}
|
|
298
|
+
static snapToValidVideoDimensions(width, height) {
|
|
299
|
+
const supported = [
|
|
300
|
+
[1280, 720],
|
|
301
|
+
[720, 1280]
|
|
302
|
+
];
|
|
303
|
+
const targetRatio = width / height;
|
|
304
|
+
const targetArea = width * height;
|
|
305
|
+
const score = ([w, h]) => {
|
|
306
|
+
const ratioDiff = Math.abs(w / h - targetRatio);
|
|
307
|
+
const areaDiff = Math.abs(w * h - targetArea) / Math.max(w * h, targetArea);
|
|
308
|
+
return ratioDiff * 3 + areaDiff;
|
|
309
|
+
};
|
|
310
|
+
const best = supported.reduce((bestSoFar, candidate) => score(candidate) < score(bestSoFar) ? candidate : bestSoFar);
|
|
311
|
+
return `${best[0]}x${best[1]}`;
|
|
312
|
+
}
|
|
313
|
+
static extractImageDimensions(image) {
|
|
314
|
+
// PNG IHDR
|
|
315
|
+
if (image.length >= 24 &&
|
|
316
|
+
image[0] === 0x89 &&
|
|
317
|
+
image[1] === 0x50 &&
|
|
318
|
+
image[2] === 0x4e &&
|
|
319
|
+
image[3] === 0x47) {
|
|
320
|
+
const view = new DataView(image.buffer, image.byteOffset, image.byteLength);
|
|
321
|
+
const width = view.getUint32(16, false);
|
|
322
|
+
const height = view.getUint32(20, false);
|
|
323
|
+
return [width, height];
|
|
324
|
+
}
|
|
325
|
+
// JPEG SOF marker scan
|
|
326
|
+
if (image.length >= 4 && image[0] === 0xff && image[1] === 0xd8) {
|
|
327
|
+
let offset = 2;
|
|
328
|
+
while (offset + 9 < image.length) {
|
|
329
|
+
if (image[offset] !== 0xff) {
|
|
330
|
+
offset += 1;
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
const marker = image[offset + 1];
|
|
334
|
+
const size = (image[offset + 2] << 8) + image[offset + 3];
|
|
335
|
+
if (size < 2) {
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
if (marker >= 0xc0 && marker <= 0xc3) {
|
|
339
|
+
const height = (image[offset + 5] << 8) + image[offset + 6];
|
|
340
|
+
const width = (image[offset + 7] << 8) + image[offset + 8];
|
|
341
|
+
return [width, height];
|
|
342
|
+
}
|
|
343
|
+
offset += 2 + size;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
throw new Error("Unsupported image format for dimension extraction");
|
|
347
|
+
}
|
|
348
|
+
async uriToBase64(uri) {
|
|
349
|
+
const resolved = await this.resolveUri(uri);
|
|
350
|
+
if (resolved.startsWith("data:")) {
|
|
351
|
+
return this.normalizeDataUri(resolved);
|
|
352
|
+
}
|
|
353
|
+
const response = await this._fetch(resolved);
|
|
354
|
+
if (!response.ok) {
|
|
355
|
+
throw new Error(`Failed to fetch URI: ${response.status}`);
|
|
356
|
+
}
|
|
357
|
+
const mime = response.headers.get("content-type") ?? "application/octet-stream";
|
|
358
|
+
const data = new Uint8Array(await response.arrayBuffer());
|
|
359
|
+
return makeDataUri(mime, data);
|
|
360
|
+
}
|
|
361
|
+
normalizeDataUri(uri) {
|
|
362
|
+
const { mime, data } = parseDataUri(uri);
|
|
363
|
+
return makeDataUri(mime, data);
|
|
364
|
+
}
|
|
365
|
+
async messageContentToOpenAIContentPart(content) {
|
|
366
|
+
if (content.type === "text") {
|
|
367
|
+
const c = content;
|
|
368
|
+
return { type: "text", text: c.text };
|
|
369
|
+
}
|
|
370
|
+
if (content.type === "audio") {
|
|
371
|
+
const c = content;
|
|
372
|
+
const base64 = c.audio.uri
|
|
373
|
+
? (await this.uriToBase64(c.audio.uri)).split(",", 2)[1]
|
|
374
|
+
: Buffer.from(asUint8Array(c.audio.data ?? new Uint8Array())).toString("base64");
|
|
375
|
+
return {
|
|
376
|
+
type: "input_audio",
|
|
377
|
+
input_audio: {
|
|
378
|
+
format: "mp3",
|
|
379
|
+
data: base64
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
const c = content;
|
|
384
|
+
const imageUrl = c.image.uri
|
|
385
|
+
? await this.uriToBase64(c.image.uri)
|
|
386
|
+
: makeDataUri(c.image.mimeType ?? "image/jpeg", asUint8Array(c.image.data ?? new Uint8Array()));
|
|
387
|
+
return {
|
|
388
|
+
type: "image_url",
|
|
389
|
+
image_url: { url: imageUrl }
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
async convertMessage(message) {
|
|
393
|
+
if (message.role === "tool") {
|
|
394
|
+
if (!message.toolCallId) {
|
|
395
|
+
throw new Error("Tool message requires toolCallId");
|
|
396
|
+
}
|
|
397
|
+
let content = "";
|
|
398
|
+
if (typeof message.content === "string") {
|
|
399
|
+
content = message.content;
|
|
400
|
+
}
|
|
401
|
+
else if (message.content != null) {
|
|
402
|
+
content = JSON.stringify(message.content, defaultSerializer);
|
|
403
|
+
}
|
|
404
|
+
return {
|
|
405
|
+
role: "tool",
|
|
406
|
+
content,
|
|
407
|
+
tool_call_id: message.toolCallId
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
if (message.role === "system") {
|
|
411
|
+
return {
|
|
412
|
+
role: "system",
|
|
413
|
+
content: typeof message.content === "string" ? message.content : ""
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
if (message.role === "assistant") {
|
|
417
|
+
const toolCalls = (message.toolCalls ?? []).map((tc) => ({
|
|
418
|
+
type: "function",
|
|
419
|
+
id: tc.id,
|
|
420
|
+
function: {
|
|
421
|
+
name: tc.name,
|
|
422
|
+
arguments: JSON.stringify(tc.args, defaultSerializer)
|
|
423
|
+
}
|
|
424
|
+
}));
|
|
425
|
+
if (typeof message.content === "string" || message.content == null) {
|
|
426
|
+
return {
|
|
427
|
+
role: "assistant",
|
|
428
|
+
content: message.content,
|
|
429
|
+
...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {})
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
const parts = await Promise.all(message.content.map((part) => this.messageContentToOpenAIContentPart(part)));
|
|
433
|
+
return {
|
|
434
|
+
role: "assistant",
|
|
435
|
+
content: parts,
|
|
436
|
+
...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {})
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
if (message.role !== "user") {
|
|
440
|
+
throw new Error(`Unsupported role: ${message.role}`);
|
|
441
|
+
}
|
|
442
|
+
if (typeof message.content === "string") {
|
|
443
|
+
return {
|
|
444
|
+
role: "user",
|
|
445
|
+
content: message.content
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
const parts = await Promise.all((message.content ?? []).map((part) => this.messageContentToOpenAIContentPart(part)));
|
|
449
|
+
return {
|
|
450
|
+
role: "user",
|
|
451
|
+
content: parts
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
formatTools(tools) {
|
|
455
|
+
return tools.map((tool) => {
|
|
456
|
+
if (tool.type === "code_interpreter" ||
|
|
457
|
+
tool.name === "code_interpreter") {
|
|
458
|
+
return { type: "code_interpreter" };
|
|
459
|
+
}
|
|
460
|
+
return {
|
|
461
|
+
type: "function",
|
|
462
|
+
function: {
|
|
463
|
+
name: tool.name,
|
|
464
|
+
description: tool.description ?? "",
|
|
465
|
+
parameters: tool.inputSchema ?? { type: "object", properties: {} }
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
convertSystemToUserForOModels(messages, model) {
|
|
471
|
+
if (!model.startsWith("o")) {
|
|
472
|
+
return messages;
|
|
473
|
+
}
|
|
474
|
+
return messages.map((msg) => msg.role === "system"
|
|
475
|
+
? {
|
|
476
|
+
...msg,
|
|
477
|
+
role: "user",
|
|
478
|
+
content: `Instructions: ${typeof msg.content === "string" ? msg.content : ""}`
|
|
479
|
+
}
|
|
480
|
+
: msg);
|
|
481
|
+
}
|
|
482
|
+
async *generateMessages(args) {
|
|
483
|
+
const { model, tools = [], maxTokens = 16384, temperature, topP, presencePenalty, frequencyPenalty, audio } = args;
|
|
484
|
+
const messages = this.convertSystemToUserForOModels(args.messages, model);
|
|
485
|
+
const openaiMessages = await Promise.all(messages.map((m) => this.convertMessage(m)));
|
|
486
|
+
const request = {
|
|
487
|
+
model,
|
|
488
|
+
messages: openaiMessages,
|
|
489
|
+
max_completion_tokens: maxTokens,
|
|
490
|
+
stream: true,
|
|
491
|
+
stream_options: { include_usage: true }
|
|
492
|
+
};
|
|
493
|
+
const hasTools = tools.length > 0 && (await this.hasToolSupport(model));
|
|
494
|
+
if (temperature != null)
|
|
495
|
+
request.temperature = temperature;
|
|
496
|
+
if (topP != null)
|
|
497
|
+
request.top_p = topP;
|
|
498
|
+
if (presencePenalty != null)
|
|
499
|
+
request.presence_penalty = presencePenalty;
|
|
500
|
+
if (frequencyPenalty != null)
|
|
501
|
+
request.frequency_penalty = frequencyPenalty;
|
|
502
|
+
if (audio) {
|
|
503
|
+
request.audio = audio;
|
|
504
|
+
request.modalities = ["text", "audio"];
|
|
505
|
+
}
|
|
506
|
+
if (hasTools) {
|
|
507
|
+
request.tools = this.formatTools(tools);
|
|
508
|
+
}
|
|
509
|
+
log.debug("OpenAI request", { model });
|
|
510
|
+
const stream = (await this.getClient().chat.completions.create(request));
|
|
511
|
+
const deltaToolCalls = new Map();
|
|
512
|
+
try {
|
|
513
|
+
for await (const chunk of stream) {
|
|
514
|
+
if (chunk?.usage) {
|
|
515
|
+
this.trackUsage(model, {
|
|
516
|
+
inputTokens: chunk.usage.prompt_tokens ?? 0,
|
|
517
|
+
outputTokens: chunk.usage.completion_tokens ?? 0,
|
|
518
|
+
cachedTokens: chunk.usage.prompt_tokens_details?.cached_tokens ?? 0
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
const choice = chunk?.choices?.[0];
|
|
522
|
+
if (!choice)
|
|
523
|
+
continue;
|
|
524
|
+
const delta = choice.delta;
|
|
525
|
+
if (delta?.audio?.data) {
|
|
526
|
+
const audioChunk = {
|
|
527
|
+
type: "chunk",
|
|
528
|
+
content_type: "audio",
|
|
529
|
+
content: String(delta.audio.data)
|
|
530
|
+
};
|
|
531
|
+
yield audioChunk;
|
|
532
|
+
}
|
|
533
|
+
if (Array.isArray(delta?.tool_calls)) {
|
|
534
|
+
for (const tc of delta.tool_calls) {
|
|
535
|
+
const index = Number(tc.index ?? 0);
|
|
536
|
+
const current = deltaToolCalls.get(index) ?? {
|
|
537
|
+
id: String(tc.id ?? ""),
|
|
538
|
+
name: String(tc.function?.name ?? ""),
|
|
539
|
+
arguments: ""
|
|
540
|
+
};
|
|
541
|
+
if (tc.id)
|
|
542
|
+
current.id = String(tc.id);
|
|
543
|
+
if (tc.function?.name)
|
|
544
|
+
current.name = String(tc.function.name);
|
|
545
|
+
if (tc.function?.arguments)
|
|
546
|
+
current.arguments += String(tc.function.arguments);
|
|
547
|
+
deltaToolCalls.set(index, current);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
if (delta?.content !== undefined || choice.finish_reason === "stop") {
|
|
551
|
+
const item = {
|
|
552
|
+
type: "chunk",
|
|
553
|
+
content: String(delta?.content ?? ""),
|
|
554
|
+
done: choice.finish_reason === "stop"
|
|
555
|
+
};
|
|
556
|
+
yield item;
|
|
557
|
+
}
|
|
558
|
+
if (choice.finish_reason === "tool_calls") {
|
|
559
|
+
for (const call of deltaToolCalls.values()) {
|
|
560
|
+
const toolCall = this.buildToolCall(call.id, call.name, call.arguments);
|
|
561
|
+
yield toolCall;
|
|
562
|
+
}
|
|
563
|
+
deltaToolCalls.clear();
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
finally {
|
|
568
|
+
if (typeof stream.close === "function") {
|
|
569
|
+
await stream.close();
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
async generateMessage(args) {
|
|
574
|
+
const { model, tools = [], toolChoice, maxTokens = 16384, temperature, topP, presencePenalty, frequencyPenalty } = args;
|
|
575
|
+
const messages = this.convertSystemToUserForOModels(args.messages, model);
|
|
576
|
+
const openaiMessages = await Promise.all(messages.map((m) => this.convertMessage(m)));
|
|
577
|
+
const request = {
|
|
578
|
+
model,
|
|
579
|
+
messages: openaiMessages,
|
|
580
|
+
stream: false,
|
|
581
|
+
max_completion_tokens: maxTokens
|
|
582
|
+
};
|
|
583
|
+
const hasTools = tools.length > 0 && (await this.hasToolSupport(model));
|
|
584
|
+
if (temperature != null)
|
|
585
|
+
request.temperature = temperature;
|
|
586
|
+
if (topP != null)
|
|
587
|
+
request.top_p = topP;
|
|
588
|
+
if (presencePenalty != null)
|
|
589
|
+
request.presence_penalty = presencePenalty;
|
|
590
|
+
if (frequencyPenalty != null)
|
|
591
|
+
request.frequency_penalty = frequencyPenalty;
|
|
592
|
+
if (hasTools) {
|
|
593
|
+
request.tools = this.formatTools(tools);
|
|
594
|
+
if (toolChoice) {
|
|
595
|
+
request.tool_choice =
|
|
596
|
+
toolChoice === "any"
|
|
597
|
+
? "required"
|
|
598
|
+
: { type: "function", function: { name: toolChoice } };
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
log.debug("OpenAI request", { model });
|
|
602
|
+
const completion = await this.getClient().chat.completions.create(request);
|
|
603
|
+
const choice = completion.choices?.[0];
|
|
604
|
+
if (!choice) {
|
|
605
|
+
throw new Error("OpenAI returned no choices");
|
|
606
|
+
}
|
|
607
|
+
const usage = completion.usage;
|
|
608
|
+
if (usage) {
|
|
609
|
+
this.trackUsage(model, {
|
|
610
|
+
inputTokens: usage.prompt_tokens ?? 0,
|
|
611
|
+
outputTokens: usage.completion_tokens ?? 0,
|
|
612
|
+
cachedTokens: usage.prompt_tokens_details?.cached_tokens ?? 0
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
const responseMessage = choice.message;
|
|
616
|
+
const toolCalls = Array.isArray(responseMessage.tool_calls)
|
|
617
|
+
? responseMessage.tool_calls.map((tc) => this.buildToolCall(tc.id, tc.function.name, tc.function.arguments))
|
|
618
|
+
: undefined;
|
|
619
|
+
return {
|
|
620
|
+
role: "assistant",
|
|
621
|
+
content: responseMessage.content ?? null,
|
|
622
|
+
toolCalls
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
async textToImage(params) {
|
|
626
|
+
if (!params.prompt) {
|
|
627
|
+
throw new Error("The input prompt cannot be empty.");
|
|
628
|
+
}
|
|
629
|
+
const prompt = params.negativePrompt
|
|
630
|
+
? `${params.prompt.trim()}\n\nDo not include: ${params.negativePrompt.trim()}`
|
|
631
|
+
: params.prompt;
|
|
632
|
+
const request = {
|
|
633
|
+
model: params.model.id,
|
|
634
|
+
prompt
|
|
635
|
+
};
|
|
636
|
+
const size = this.resolveImageSize(params.width ?? undefined, params.height ?? undefined);
|
|
637
|
+
if (size)
|
|
638
|
+
request.size = size;
|
|
639
|
+
if (params.quality)
|
|
640
|
+
request.quality = params.quality;
|
|
641
|
+
const response = await this.getClient().images.generate(request);
|
|
642
|
+
const item = response.data?.[0];
|
|
643
|
+
if (!item) {
|
|
644
|
+
throw new Error("OpenAI image generation returned no image data.");
|
|
645
|
+
}
|
|
646
|
+
if (item.b64_json) {
|
|
647
|
+
return Uint8Array.from(Buffer.from(item.b64_json, "base64"));
|
|
648
|
+
}
|
|
649
|
+
if (item.url) {
|
|
650
|
+
const fetchResponse = await this._fetch(item.url);
|
|
651
|
+
if (!fetchResponse.ok) {
|
|
652
|
+
throw new Error(`Image fetch failed: ${fetchResponse.status}`);
|
|
653
|
+
}
|
|
654
|
+
return new Uint8Array(await fetchResponse.arrayBuffer());
|
|
655
|
+
}
|
|
656
|
+
throw new Error("OpenAI image generation returned no image data.");
|
|
657
|
+
}
|
|
658
|
+
async imageToImage(image, params) {
|
|
659
|
+
if (!image || image.length === 0) {
|
|
660
|
+
throw new Error("image must not be empty.");
|
|
661
|
+
}
|
|
662
|
+
if (!params.prompt) {
|
|
663
|
+
throw new Error("The input prompt cannot be empty.");
|
|
664
|
+
}
|
|
665
|
+
const prompt = params.negativePrompt
|
|
666
|
+
? `${params.prompt.trim()}\n\nDo not include: ${params.negativePrompt.trim()}`
|
|
667
|
+
: params.prompt;
|
|
668
|
+
const request = {
|
|
669
|
+
model: params.model.id,
|
|
670
|
+
image: ["image.png", image, "image/png"],
|
|
671
|
+
prompt
|
|
672
|
+
};
|
|
673
|
+
const size = this.resolveImageSize(params.targetWidth ?? undefined, params.targetHeight ?? undefined);
|
|
674
|
+
if (size)
|
|
675
|
+
request.size = size;
|
|
676
|
+
if (params.quality)
|
|
677
|
+
request.quality = params.quality;
|
|
678
|
+
const response = await this.getClient().images.edit(request);
|
|
679
|
+
const item = response.data?.[0];
|
|
680
|
+
if (!item) {
|
|
681
|
+
throw new Error("OpenAI image editing returned no image data.");
|
|
682
|
+
}
|
|
683
|
+
if (item.b64_json) {
|
|
684
|
+
return Uint8Array.from(Buffer.from(item.b64_json, "base64"));
|
|
685
|
+
}
|
|
686
|
+
if (item.url) {
|
|
687
|
+
const fetchResponse = await this._fetch(item.url);
|
|
688
|
+
if (!fetchResponse.ok) {
|
|
689
|
+
throw new Error(`Image fetch failed: ${fetchResponse.status}`);
|
|
690
|
+
}
|
|
691
|
+
return new Uint8Array(await fetchResponse.arrayBuffer());
|
|
692
|
+
}
|
|
693
|
+
throw new Error("OpenAI image editing returned no image data.");
|
|
694
|
+
}
|
|
695
|
+
async *textToSpeech(args) {
|
|
696
|
+
if (!args.text) {
|
|
697
|
+
throw new Error("text must not be empty");
|
|
698
|
+
}
|
|
699
|
+
const voice = args.voice ?? "alloy";
|
|
700
|
+
const speed = Math.max(0.25, Math.min(4.0, args.speed ?? 1.0));
|
|
701
|
+
const speechApi = this.getClient().audio.speech;
|
|
702
|
+
if (speechApi.with_streaming_response?.create) {
|
|
703
|
+
const response = await speechApi.with_streaming_response.create({
|
|
704
|
+
model: args.model,
|
|
705
|
+
input: args.text,
|
|
706
|
+
voice,
|
|
707
|
+
speed,
|
|
708
|
+
response_format: "pcm"
|
|
709
|
+
});
|
|
710
|
+
for await (const chunk of response.iterBytes(4096)) {
|
|
711
|
+
const bytes = asUint8Array(chunk);
|
|
712
|
+
yield { samples: toInt16Samples(bytes) };
|
|
713
|
+
}
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
const response = await speechApi.create({
|
|
717
|
+
model: args.model,
|
|
718
|
+
input: args.text,
|
|
719
|
+
voice,
|
|
720
|
+
speed,
|
|
721
|
+
response_format: "pcm"
|
|
722
|
+
});
|
|
723
|
+
const bytes = asUint8Array(typeof response.arrayBuffer === "function"
|
|
724
|
+
? await response.arrayBuffer()
|
|
725
|
+
: response);
|
|
726
|
+
yield { samples: toInt16Samples(bytes) };
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* OpenAI supports several fully-encoded TTS formats directly (mp3, opus,
|
|
730
|
+
* aac, flac, wav). Use this path when the caller has requested one of them
|
|
731
|
+
* so we can skip the PCM → WAV wrapping step and honor the user's choice.
|
|
732
|
+
* For `"pcm"` (and anything unrecognised) we return `null` and let the
|
|
733
|
+
* streaming PCM path handle it.
|
|
734
|
+
*/
|
|
735
|
+
async textToSpeechEncoded(args) {
|
|
736
|
+
if (!args.text) {
|
|
737
|
+
throw new Error("text must not be empty");
|
|
738
|
+
}
|
|
739
|
+
const fmt = (args.audioFormat ?? "").toLowerCase();
|
|
740
|
+
const formatToMime = {
|
|
741
|
+
mp3: "audio/mpeg",
|
|
742
|
+
opus: "audio/ogg",
|
|
743
|
+
aac: "audio/aac",
|
|
744
|
+
flac: "audio/flac",
|
|
745
|
+
wav: "audio/wav"
|
|
746
|
+
};
|
|
747
|
+
if (!(fmt in formatToMime)) {
|
|
748
|
+
return null;
|
|
749
|
+
}
|
|
750
|
+
const voice = args.voice ?? "alloy";
|
|
751
|
+
const speed = Math.max(0.25, Math.min(4.0, args.speed ?? 1.0));
|
|
752
|
+
const speechApi = this.getClient().audio.speech;
|
|
753
|
+
const response = await speechApi.create({
|
|
754
|
+
model: args.model,
|
|
755
|
+
input: args.text,
|
|
756
|
+
voice,
|
|
757
|
+
speed,
|
|
758
|
+
response_format: fmt
|
|
759
|
+
});
|
|
760
|
+
const bytes = asUint8Array(typeof response.arrayBuffer === "function"
|
|
761
|
+
? await response.arrayBuffer()
|
|
762
|
+
: response);
|
|
763
|
+
return { data: bytes, mimeType: formatToMime[fmt] };
|
|
764
|
+
}
|
|
765
|
+
async automaticSpeechRecognition(args) {
|
|
766
|
+
if (!args.audio || args.audio.length === 0) {
|
|
767
|
+
throw new Error("audio must not be empty");
|
|
768
|
+
}
|
|
769
|
+
const temperature = Math.max(0, Math.min(1, args.temperature ?? 0));
|
|
770
|
+
const audioPart = new Uint8Array(args.audio);
|
|
771
|
+
const fileLike = typeof File !== "undefined"
|
|
772
|
+
? new File([audioPart], "audio.mp3", { type: "audio/mpeg" })
|
|
773
|
+
: Object.assign(new Blob([audioPart], { type: "audio/mpeg" }), {
|
|
774
|
+
name: "audio.mp3"
|
|
775
|
+
});
|
|
776
|
+
const requestParams = {
|
|
777
|
+
file: fileLike,
|
|
778
|
+
model: args.model,
|
|
779
|
+
language: args.language,
|
|
780
|
+
prompt: args.prompt,
|
|
781
|
+
temperature
|
|
782
|
+
};
|
|
783
|
+
if (args.word_timestamps) {
|
|
784
|
+
requestParams.response_format = "verbose_json";
|
|
785
|
+
requestParams.timestamp_granularities = ["word", "segment"];
|
|
786
|
+
}
|
|
787
|
+
const response = await this.getClient().audio.transcriptions.create(requestParams);
|
|
788
|
+
const text = String(response.text ?? "");
|
|
789
|
+
if (!args.word_timestamps) {
|
|
790
|
+
return { text };
|
|
791
|
+
}
|
|
792
|
+
const chunks = [];
|
|
793
|
+
const rawWords = response.words;
|
|
794
|
+
if (rawWords) {
|
|
795
|
+
for (const w of rawWords) {
|
|
796
|
+
chunks.push({
|
|
797
|
+
timestamp: [w.start, w.end],
|
|
798
|
+
text: w.word
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
else {
|
|
803
|
+
const rawSegments = response.segments;
|
|
804
|
+
if (rawSegments) {
|
|
805
|
+
for (const s of rawSegments) {
|
|
806
|
+
chunks.push({
|
|
807
|
+
timestamp: [s.start, s.end],
|
|
808
|
+
text: s.text
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
return { text, chunks: chunks.length > 0 ? chunks : undefined };
|
|
814
|
+
}
|
|
815
|
+
async textToVideo(params) {
|
|
816
|
+
if (!params.prompt) {
|
|
817
|
+
throw new Error("The input prompt cannot be empty.");
|
|
818
|
+
}
|
|
819
|
+
const size = OpenAIProvider.resolveVideoSize(params.aspectRatio, params.resolution);
|
|
820
|
+
const seconds = OpenAIProvider.secondsFromParams(params) ?? 4;
|
|
821
|
+
const request = {
|
|
822
|
+
model: params.model.id,
|
|
823
|
+
prompt: params.prompt,
|
|
824
|
+
size: size ?? "1024x1024",
|
|
825
|
+
seconds: String(seconds)
|
|
826
|
+
};
|
|
827
|
+
const video = await this.getClient().videos.create(request);
|
|
828
|
+
if (!video?.id) {
|
|
829
|
+
throw new Error("OpenAI video create response did not contain a video id");
|
|
830
|
+
}
|
|
831
|
+
const timeoutMs = 10 * 60 * 1000;
|
|
832
|
+
const intervalMs = 2000;
|
|
833
|
+
const start = Date.now();
|
|
834
|
+
let latest = video;
|
|
835
|
+
while (latest.status === "queued" || latest.status === "in_progress") {
|
|
836
|
+
if (Date.now() - start > timeoutMs) {
|
|
837
|
+
throw new Error("Video generation timed out");
|
|
838
|
+
}
|
|
839
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
840
|
+
latest = await this.getClient().videos.retrieve({
|
|
841
|
+
video_id: video.id
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
if (latest.status !== "completed") {
|
|
845
|
+
throw new Error(String(latest.error ??
|
|
846
|
+
`Video generation ended with status '${latest.status}'`));
|
|
847
|
+
}
|
|
848
|
+
const bytes = await this.getClient().get(`/videos/${video.id}/content`, {
|
|
849
|
+
cast_to: Uint8Array,
|
|
850
|
+
options: {
|
|
851
|
+
params: { variant: "video" }
|
|
852
|
+
}
|
|
853
|
+
});
|
|
854
|
+
return asUint8Array(bytes);
|
|
855
|
+
}
|
|
856
|
+
async imageToVideo(image, params) {
|
|
857
|
+
if (!image || image.length === 0) {
|
|
858
|
+
throw new Error("The input image cannot be empty.");
|
|
859
|
+
}
|
|
860
|
+
const [width, height] = OpenAIProvider.extractImageDimensions(image);
|
|
861
|
+
const size = OpenAIProvider.snapToValidVideoDimensions(width, height);
|
|
862
|
+
const seconds = OpenAIProvider.secondsFromParams(params) ?? 4;
|
|
863
|
+
const [ext] = size.startsWith("720x1280") ? ["png"] : ["png"];
|
|
864
|
+
const mimeType = `image/${ext}`;
|
|
865
|
+
const video = await this.getClient().videos.create({
|
|
866
|
+
model: params.model.id,
|
|
867
|
+
prompt: params.prompt ?? "",
|
|
868
|
+
input_reference: [`input_image.${ext}`, image, mimeType],
|
|
869
|
+
size,
|
|
870
|
+
seconds: String(seconds)
|
|
871
|
+
});
|
|
872
|
+
if (!video?.id) {
|
|
873
|
+
throw new Error("OpenAI image-to-video create response did not contain a video id");
|
|
874
|
+
}
|
|
875
|
+
const timeoutMs = 10 * 60 * 1000;
|
|
876
|
+
const intervalMs = 2000;
|
|
877
|
+
const start = Date.now();
|
|
878
|
+
let latest = video;
|
|
879
|
+
while (latest.status === "queued" || latest.status === "in_progress") {
|
|
880
|
+
if (Date.now() - start > timeoutMs) {
|
|
881
|
+
throw new Error("Image-to-video generation timed out");
|
|
882
|
+
}
|
|
883
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
884
|
+
latest = await this.getClient().videos.retrieve({
|
|
885
|
+
video_id: video.id
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
if (latest.status !== "completed") {
|
|
889
|
+
throw new Error(String(latest.error ??
|
|
890
|
+
`Image-to-video generation ended with status '${latest.status}'`));
|
|
891
|
+
}
|
|
892
|
+
const bytes = await this.getClient().get(`/videos/${video.id}/content`, {
|
|
893
|
+
cast_to: Uint8Array,
|
|
894
|
+
options: {
|
|
895
|
+
params: { variant: "video" }
|
|
896
|
+
}
|
|
897
|
+
});
|
|
898
|
+
return asUint8Array(bytes);
|
|
899
|
+
}
|
|
900
|
+
async generateEmbedding(args) {
|
|
901
|
+
const input = Array.isArray(args.text) ? args.text : [args.text];
|
|
902
|
+
if (input.length === 0 || input.every((v) => !v)) {
|
|
903
|
+
throw new Error("text must not be empty");
|
|
904
|
+
}
|
|
905
|
+
const response = await this.getClient().embeddings.create({
|
|
906
|
+
model: args.model,
|
|
907
|
+
input,
|
|
908
|
+
...(args.dimensions ? { dimensions: args.dimensions } : {})
|
|
909
|
+
});
|
|
910
|
+
return response.data.map((row) => row.embedding);
|
|
911
|
+
}
|
|
912
|
+
isContextLengthError(error) {
|
|
913
|
+
const msg = String(error).toLowerCase();
|
|
914
|
+
return msg.includes("context length") || msg.includes("maximum context");
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
//# sourceMappingURL=openai-provider.js.map
|