pi-meta-oauth 0.4.4 → 0.6.0
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/README.md +16 -69
- package/extensions/meta.ts +85 -37
- package/package.json +5 -12
- package/extensions/media/files.ts +0 -131
- package/extensions/media/limits.ts +0 -12
- package/extensions/media/mime.ts +0 -178
- package/extensions/media/payload.ts +0 -303
- package/extensions/media/responses.ts +0 -186
- package/extensions/media-output.ts +0 -127
- package/extensions/media.ts +0 -798
- package/extensions/voice/Entitlements.plist +0 -8
- package/extensions/voice/Info.plist +0 -22
- package/extensions/voice/asr.ts +0 -107
- package/extensions/voice/helpers.ts +0 -243
- package/extensions/voice/macos-audio.swift +0 -172
- package/extensions/voice/windows-audio.cs +0 -281
- package/extensions/voice/windows-audio.ps1 +0 -195
- package/extensions/voice.ts +0 -680
package/extensions/media/mime.ts
DELETED
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
import { extname } from "node:path";
|
|
2
|
-
|
|
3
|
-
export interface ResponsesContentBlock {
|
|
4
|
-
type: string;
|
|
5
|
-
[key: string]: unknown;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
type SupportedMime =
|
|
9
|
-
| "video/mp4"
|
|
10
|
-
| "audio/mpeg"
|
|
11
|
-
| "audio/wav"
|
|
12
|
-
| "application/pdf"
|
|
13
|
-
| "image/png"
|
|
14
|
-
| "image/jpeg"
|
|
15
|
-
| "image/gif"
|
|
16
|
-
| "image/webp"
|
|
17
|
-
| "image/x-icon"
|
|
18
|
-
| "text/plain"
|
|
19
|
-
| "application/json"
|
|
20
|
-
| "application/jsonl";
|
|
21
|
-
|
|
22
|
-
const EXT_TO_MIME: Record<string, SupportedMime> = {
|
|
23
|
-
".mp4": "video/mp4",
|
|
24
|
-
".m4v": "video/mp4",
|
|
25
|
-
".mp3": "audio/mpeg",
|
|
26
|
-
".wav": "audio/wav",
|
|
27
|
-
".pdf": "application/pdf",
|
|
28
|
-
".png": "image/png",
|
|
29
|
-
".jpg": "image/jpeg",
|
|
30
|
-
".jpeg": "image/jpeg",
|
|
31
|
-
".gif": "image/gif",
|
|
32
|
-
".webp": "image/webp",
|
|
33
|
-
".ico": "image/x-icon",
|
|
34
|
-
".txt": "text/plain",
|
|
35
|
-
".json": "application/json",
|
|
36
|
-
".jsonl": "application/jsonl",
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
export function mimeForPath(path: string): SupportedMime | undefined {
|
|
40
|
-
const ext = extname(path).toLowerCase();
|
|
41
|
-
return EXT_TO_MIME[ext];
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function mimeForDataUrl(dataUrl: string): string | undefined {
|
|
45
|
-
const m = dataUrl.match(/^data:([^;,\s]+)[;,]/);
|
|
46
|
-
return m?.[1]?.toLowerCase();
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function isVideoMime(m: string | undefined): boolean {
|
|
50
|
-
return m === "video/mp4";
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function isAudioMime(m: string | undefined): boolean {
|
|
54
|
-
return (
|
|
55
|
-
m === "audio/mpeg" ||
|
|
56
|
-
m === "audio/mp3" ||
|
|
57
|
-
m === "audio/wav" ||
|
|
58
|
-
m === "audio/x-wav"
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function isPdfMime(m: string | undefined): boolean {
|
|
63
|
-
return m === "application/pdf";
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function isImageMime(m: string | undefined): boolean {
|
|
67
|
-
return typeof m === "string" && m.startsWith("image/");
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export function filenameForMime(
|
|
71
|
-
mime: string | undefined,
|
|
72
|
-
fallback = "file.bin",
|
|
73
|
-
): string {
|
|
74
|
-
switch (mime) {
|
|
75
|
-
case "video/mp4":
|
|
76
|
-
return "video.mp4";
|
|
77
|
-
case "audio/mpeg":
|
|
78
|
-
case "audio/mp3":
|
|
79
|
-
return "audio.mp3";
|
|
80
|
-
case "audio/wav":
|
|
81
|
-
case "audio/x-wav":
|
|
82
|
-
return "audio.wav";
|
|
83
|
-
case "application/pdf":
|
|
84
|
-
return "document.pdf";
|
|
85
|
-
default:
|
|
86
|
-
return fallback;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export function audioFormatForMime(mime: string | undefined): "wav" | "mp3" {
|
|
91
|
-
return mime === "audio/wav" || mime === "audio/x-wav" ? "wav" : "mp3";
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export function base64FromDataUrl(dataUrl: string): string {
|
|
95
|
-
const comma = dataUrl.indexOf(",");
|
|
96
|
-
return comma === -1 ? "" : dataUrl.slice(comma + 1);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export function base64ByteLength(base64: string): number {
|
|
100
|
-
const normalized = base64.replace(/\s/g, "");
|
|
101
|
-
if (!normalized) return 0;
|
|
102
|
-
const padding = normalized.endsWith("==")
|
|
103
|
-
? 2
|
|
104
|
-
: normalized.endsWith("=")
|
|
105
|
-
? 1
|
|
106
|
-
: 0;
|
|
107
|
-
return Math.max(0, Math.floor((normalized.length * 3) / 4) - padding);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export function isRemoteMediaSource(source: string): boolean {
|
|
111
|
-
return source.startsWith("https://") || source.startsWith("data:");
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export function mediaPathFromUrl(url: string): string {
|
|
115
|
-
if (url.startsWith("data:")) return url;
|
|
116
|
-
try {
|
|
117
|
-
return new URL(url).pathname;
|
|
118
|
-
} catch {
|
|
119
|
-
const withoutHash = url.split("#")[0] ?? url;
|
|
120
|
-
return withoutHash.split("?")[0] ?? withoutHash;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export function mimeForSource(source: string): string | undefined {
|
|
125
|
-
if (source.startsWith("data:")) return mimeForDataUrl(source);
|
|
126
|
-
if (source.startsWith("https://")) {
|
|
127
|
-
try {
|
|
128
|
-
return mimeForPath(new URL(source).pathname);
|
|
129
|
-
} catch {
|
|
130
|
-
return undefined;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
return mimeForPath(source);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/** Build a generic Meta Responses input_file block. */
|
|
137
|
-
export function inputFileFromSource(
|
|
138
|
-
source: string,
|
|
139
|
-
filename?: string,
|
|
140
|
-
): ResponsesContentBlock {
|
|
141
|
-
if (source.startsWith("https://")) {
|
|
142
|
-
return { type: "input_file", file_url: source };
|
|
143
|
-
}
|
|
144
|
-
if (source.startsWith("data:")) {
|
|
145
|
-
const mime = mimeForDataUrl(source);
|
|
146
|
-
return {
|
|
147
|
-
type: "input_file",
|
|
148
|
-
file_data: source,
|
|
149
|
-
filename: filename ?? filenameForMime(mime),
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
return { type: "input_file", file_id: source };
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/** Build the typed content block required by Meta for each media kind. */
|
|
156
|
-
export function mediaInputFromSource(
|
|
157
|
-
source: string,
|
|
158
|
-
filename?: string,
|
|
159
|
-
expectedMime?: string,
|
|
160
|
-
): ResponsesContentBlock {
|
|
161
|
-
if (!source.startsWith("data:") && !source.startsWith("https://")) {
|
|
162
|
-
return { type: "input_file", file_id: source };
|
|
163
|
-
}
|
|
164
|
-
const mime = expectedMime ?? mimeForSource(source);
|
|
165
|
-
if (isVideoMime(mime)) {
|
|
166
|
-
return { type: "input_video", video_url: source };
|
|
167
|
-
}
|
|
168
|
-
if (isAudioMime(mime) && source.startsWith("data:")) {
|
|
169
|
-
return {
|
|
170
|
-
type: "input_audio",
|
|
171
|
-
input_audio: {
|
|
172
|
-
data: base64FromDataUrl(source),
|
|
173
|
-
format: audioFormatForMime(mime),
|
|
174
|
-
},
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
return inputFileFromSource(source, filename);
|
|
178
|
-
}
|
|
@@ -1,303 +0,0 @@
|
|
|
1
|
-
import { STORE_SAFE_INLINE_BYTES } from "./limits.ts";
|
|
2
|
-
import { uploadInlineMedia } from "./files.ts";
|
|
3
|
-
import {
|
|
4
|
-
base64ByteLength,
|
|
5
|
-
base64FromDataUrl,
|
|
6
|
-
filenameForMime,
|
|
7
|
-
isAudioMime,
|
|
8
|
-
isImageMime,
|
|
9
|
-
isPdfMime,
|
|
10
|
-
isVideoMime,
|
|
11
|
-
mediaInputFromSource,
|
|
12
|
-
mediaPathFromUrl,
|
|
13
|
-
mimeForDataUrl,
|
|
14
|
-
} from "./mime.ts";
|
|
15
|
-
|
|
16
|
-
function record(value: unknown): Record<string, unknown> | undefined {
|
|
17
|
-
return value !== null && typeof value === "object"
|
|
18
|
-
? (value as Record<string, unknown>)
|
|
19
|
-
: undefined;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function isInputImageBlock(b: Record<string, unknown>): boolean {
|
|
23
|
-
return b.type === "input_image" && typeof b.image_url === "string";
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function rewriteBlock(
|
|
27
|
-
block: Record<string, unknown>,
|
|
28
|
-
): Record<string, unknown> | null {
|
|
29
|
-
if (!isInputImageBlock(block)) return null;
|
|
30
|
-
const url = block.image_url as string;
|
|
31
|
-
const mime = mimeForDataUrl(url);
|
|
32
|
-
const lowerPath = mediaPathFromUrl(url).toLowerCase();
|
|
33
|
-
const isHttps = url.startsWith("https://");
|
|
34
|
-
const isHttpsVideo =
|
|
35
|
-
isHttps && (lowerPath.endsWith(".mp4") || lowerPath.endsWith(".m4v"));
|
|
36
|
-
const isHttpsAudio =
|
|
37
|
-
isHttps && (lowerPath.endsWith(".mp3") || lowerPath.endsWith(".wav"));
|
|
38
|
-
const isHttpsPdf = isHttps && lowerPath.endsWith(".pdf");
|
|
39
|
-
const isHttpsImage =
|
|
40
|
-
isHttps &&
|
|
41
|
-
(lowerPath.endsWith(".png") ||
|
|
42
|
-
lowerPath.endsWith(".jpg") ||
|
|
43
|
-
lowerPath.endsWith(".jpeg") ||
|
|
44
|
-
lowerPath.endsWith(".gif") ||
|
|
45
|
-
lowerPath.endsWith(".webp"));
|
|
46
|
-
|
|
47
|
-
if (isHttpsImage || isImageMime(mime)) return null;
|
|
48
|
-
|
|
49
|
-
if (isVideoMime(mime) || isHttpsVideo) {
|
|
50
|
-
return mediaInputFromSource(url, "video.mp4", "video/mp4") as Record<
|
|
51
|
-
string,
|
|
52
|
-
unknown
|
|
53
|
-
>;
|
|
54
|
-
}
|
|
55
|
-
if (isAudioMime(mime) || isHttpsAudio) {
|
|
56
|
-
const effectiveMime =
|
|
57
|
-
typeof mime === "string"
|
|
58
|
-
? mime
|
|
59
|
-
: lowerPath.endsWith(".wav")
|
|
60
|
-
? "audio/wav"
|
|
61
|
-
: "audio/mpeg";
|
|
62
|
-
return mediaInputFromSource(
|
|
63
|
-
url,
|
|
64
|
-
filenameForMime(effectiveMime, "audio.bin"),
|
|
65
|
-
effectiveMime,
|
|
66
|
-
) as Record<string, unknown>;
|
|
67
|
-
}
|
|
68
|
-
if (isPdfMime(mime) || isHttpsPdf) {
|
|
69
|
-
return mediaInputFromSource(
|
|
70
|
-
url,
|
|
71
|
-
"document.pdf",
|
|
72
|
-
"application/pdf",
|
|
73
|
-
) as Record<string, unknown>;
|
|
74
|
-
}
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function rewriteBlocks(
|
|
79
|
-
blocks: Record<string, unknown>[],
|
|
80
|
-
onlyImageUrls = false,
|
|
81
|
-
): { blocks: Record<string, unknown>[]; rewritten: boolean } {
|
|
82
|
-
let rewritten = false;
|
|
83
|
-
const next = blocks.map((block) => {
|
|
84
|
-
if (
|
|
85
|
-
onlyImageUrls &&
|
|
86
|
-
!(block.type === "input_image" && typeof block.image_url === "string")
|
|
87
|
-
) {
|
|
88
|
-
return block;
|
|
89
|
-
}
|
|
90
|
-
const replacement = rewriteBlock(block);
|
|
91
|
-
if (replacement) {
|
|
92
|
-
rewritten = true;
|
|
93
|
-
return replacement;
|
|
94
|
-
}
|
|
95
|
-
return block;
|
|
96
|
-
});
|
|
97
|
-
return { blocks: next, rewritten };
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** Rewrite smuggled media image_url blocks into Meta-supported typed blocks. */
|
|
101
|
-
export function rewriteResponsesPayload(payload: unknown): {
|
|
102
|
-
rewritten: boolean;
|
|
103
|
-
payload: unknown;
|
|
104
|
-
} {
|
|
105
|
-
if (!payload || typeof payload !== "object")
|
|
106
|
-
return { rewritten: false, payload };
|
|
107
|
-
const p = payload as Record<string, unknown>;
|
|
108
|
-
const input = p.input;
|
|
109
|
-
if (!Array.isArray(input)) return { rewritten: false, payload };
|
|
110
|
-
let rewritten = false;
|
|
111
|
-
const newInput = input.map((entry) => {
|
|
112
|
-
if (!entry || typeof entry !== "object") return entry;
|
|
113
|
-
const e = { ...(entry as Record<string, unknown>) };
|
|
114
|
-
if (Array.isArray(e.content)) {
|
|
115
|
-
const result = rewriteBlocks(e.content as Record<string, unknown>[]);
|
|
116
|
-
if (result.rewritten) {
|
|
117
|
-
rewritten = true;
|
|
118
|
-
e.content = result.blocks;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
if (
|
|
122
|
-
(e.type === "function_call_output" ||
|
|
123
|
-
e.type === "custom_tool_call_output") &&
|
|
124
|
-
Array.isArray(e.output)
|
|
125
|
-
) {
|
|
126
|
-
const result = rewriteBlocks(
|
|
127
|
-
e.output as Record<string, unknown>[],
|
|
128
|
-
true,
|
|
129
|
-
);
|
|
130
|
-
if (result.rewritten) {
|
|
131
|
-
rewritten = true;
|
|
132
|
-
e.output = result.blocks;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
return e;
|
|
136
|
-
});
|
|
137
|
-
if (!rewritten) return { rewritten: false, payload };
|
|
138
|
-
return { rewritten: true, payload: { ...p, input: newInput } };
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function inlineFromBlock(
|
|
142
|
-
block: Record<string, unknown>,
|
|
143
|
-
): { base64: string; mime: string; filename: string } | undefined {
|
|
144
|
-
if (
|
|
145
|
-
block.type === "input_video" &&
|
|
146
|
-
typeof block.video_url === "string" &&
|
|
147
|
-
block.video_url.startsWith("data:")
|
|
148
|
-
) {
|
|
149
|
-
return {
|
|
150
|
-
base64: base64FromDataUrl(block.video_url),
|
|
151
|
-
mime: mimeForDataUrl(block.video_url) ?? "video/mp4",
|
|
152
|
-
filename: "video.mp4",
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
if (block.type === "input_audio") {
|
|
156
|
-
const audio = record(block.input_audio);
|
|
157
|
-
if (typeof audio?.data === "string") {
|
|
158
|
-
const format = audio.format === "wav" ? "wav" : "mp3";
|
|
159
|
-
return {
|
|
160
|
-
base64: audio.data.startsWith("data:")
|
|
161
|
-
? base64FromDataUrl(audio.data)
|
|
162
|
-
: audio.data,
|
|
163
|
-
mime: format === "wav" ? "audio/wav" : "audio/mpeg",
|
|
164
|
-
filename: `audio.${format}`,
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
if (
|
|
169
|
-
block.type === "input_file" &&
|
|
170
|
-
typeof block.file_data === "string" &&
|
|
171
|
-
block.file_data.startsWith("data:")
|
|
172
|
-
) {
|
|
173
|
-
const mime = mimeForDataUrl(block.file_data) ?? "application/octet-stream";
|
|
174
|
-
return {
|
|
175
|
-
base64: base64FromDataUrl(block.file_data),
|
|
176
|
-
mime,
|
|
177
|
-
filename:
|
|
178
|
-
typeof block.filename === "string" && block.filename
|
|
179
|
-
? block.filename
|
|
180
|
-
: filenameForMime(mime),
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
if (
|
|
184
|
-
block.type === "input_image" &&
|
|
185
|
-
typeof block.image_url === "string" &&
|
|
186
|
-
block.image_url.startsWith("data:")
|
|
187
|
-
) {
|
|
188
|
-
const mime = mimeForDataUrl(block.image_url) ?? "image/png";
|
|
189
|
-
return {
|
|
190
|
-
base64: base64FromDataUrl(block.image_url),
|
|
191
|
-
mime,
|
|
192
|
-
filename: filenameForMime(mime, "image.bin"),
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
return undefined;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function isMediaBlock(block: Record<string, unknown>): boolean {
|
|
199
|
-
return (
|
|
200
|
-
block.type === "input_file" ||
|
|
201
|
-
block.type === "input_image" ||
|
|
202
|
-
block.type === "input_video" ||
|
|
203
|
-
block.type === "input_audio"
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
export function payloadContainsMedia(payload: unknown): boolean {
|
|
208
|
-
if (!payload || typeof payload !== "object") return false;
|
|
209
|
-
const input = (payload as Record<string, unknown>).input;
|
|
210
|
-
if (!Array.isArray(input)) return false;
|
|
211
|
-
for (const entry of input as Record<string, unknown>[]) {
|
|
212
|
-
if (!entry || typeof entry !== "object") continue;
|
|
213
|
-
const groups = [entry.content, entry.output];
|
|
214
|
-
for (const group of groups) {
|
|
215
|
-
if (!Array.isArray(group)) continue;
|
|
216
|
-
for (const block of group as Record<string, unknown>[]) {
|
|
217
|
-
if (block && typeof block === "object" && isMediaBlock(block))
|
|
218
|
-
return true;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
return false;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
async function promoteBlocks(
|
|
226
|
-
blocks: Record<string, unknown>[],
|
|
227
|
-
apiKey: string,
|
|
228
|
-
uploadThresholdBytes: number,
|
|
229
|
-
signal?: AbortSignal,
|
|
230
|
-
): Promise<{
|
|
231
|
-
blocks: Record<string, unknown>[];
|
|
232
|
-
uploaded: number;
|
|
233
|
-
needsStoreFalse: boolean;
|
|
234
|
-
}> {
|
|
235
|
-
let uploaded = 0;
|
|
236
|
-
let needsStoreFalse = false;
|
|
237
|
-
const next: Record<string, unknown>[] = [];
|
|
238
|
-
for (const block of blocks) {
|
|
239
|
-
const inline = inlineFromBlock(block);
|
|
240
|
-
if (inline) {
|
|
241
|
-
needsStoreFalse = true;
|
|
242
|
-
if (base64ByteLength(inline.base64) > uploadThresholdBytes) {
|
|
243
|
-
const id = await uploadInlineMedia(
|
|
244
|
-
apiKey,
|
|
245
|
-
inline.base64,
|
|
246
|
-
inline.mime,
|
|
247
|
-
inline.filename,
|
|
248
|
-
signal,
|
|
249
|
-
);
|
|
250
|
-
uploaded += 1;
|
|
251
|
-
next.push({ type: "input_file", file_id: id });
|
|
252
|
-
continue;
|
|
253
|
-
}
|
|
254
|
-
} else if (isMediaBlock(block)) {
|
|
255
|
-
needsStoreFalse = true;
|
|
256
|
-
}
|
|
257
|
-
next.push(block);
|
|
258
|
-
}
|
|
259
|
-
return { blocks: next, uploaded, needsStoreFalse };
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
export async function maybeUploadLargeInlineBlocks(
|
|
263
|
-
payload: unknown,
|
|
264
|
-
apiKey: string | undefined,
|
|
265
|
-
uploadThresholdBytes = STORE_SAFE_INLINE_BYTES,
|
|
266
|
-
signal?: AbortSignal,
|
|
267
|
-
): Promise<{ payload: unknown; uploaded: number }> {
|
|
268
|
-
if (!apiKey) return { payload, uploaded: 0 };
|
|
269
|
-
if (!payload || typeof payload !== "object") return { payload, uploaded: 0 };
|
|
270
|
-
const p = payload as Record<string, unknown>;
|
|
271
|
-
const input = p.input;
|
|
272
|
-
if (!Array.isArray(input)) return { payload, uploaded: 0 };
|
|
273
|
-
let uploaded = 0;
|
|
274
|
-
let needsStoreFalse = false;
|
|
275
|
-
const newInput: unknown[] = [];
|
|
276
|
-
|
|
277
|
-
for (const entry of input as Record<string, unknown>[]) {
|
|
278
|
-
if (!entry || typeof entry !== "object") {
|
|
279
|
-
newInput.push(entry);
|
|
280
|
-
continue;
|
|
281
|
-
}
|
|
282
|
-
const nextEntry = { ...entry };
|
|
283
|
-
for (const key of ["content", "output"] as const) {
|
|
284
|
-
if (!Array.isArray(nextEntry[key])) continue;
|
|
285
|
-
const promoted = await promoteBlocks(
|
|
286
|
-
nextEntry[key] as Record<string, unknown>[],
|
|
287
|
-
apiKey,
|
|
288
|
-
uploadThresholdBytes,
|
|
289
|
-
signal,
|
|
290
|
-
);
|
|
291
|
-
nextEntry[key] = promoted.blocks;
|
|
292
|
-
uploaded += promoted.uploaded;
|
|
293
|
-
needsStoreFalse ||= promoted.needsStoreFalse;
|
|
294
|
-
}
|
|
295
|
-
newInput.push(nextEntry);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
if (!needsStoreFalse && uploaded === 0) return { payload, uploaded: 0 };
|
|
299
|
-
return {
|
|
300
|
-
payload: { ...p, input: newInput, store: false },
|
|
301
|
-
uploaded,
|
|
302
|
-
};
|
|
303
|
-
}
|
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
import type { Usage } from "@earendil-works/pi-ai";
|
|
2
|
-
import { META_API_BASE_URL, metaFallbackCost } from "../meta.ts";
|
|
3
|
-
|
|
4
|
-
function record(value: unknown): Record<string, unknown> | undefined {
|
|
5
|
-
return value !== null && typeof value === "object"
|
|
6
|
-
? (value as Record<string, unknown>)
|
|
7
|
-
: undefined;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
function tokenCount(...values: unknown[]): number | undefined {
|
|
11
|
-
for (const value of values) {
|
|
12
|
-
const numeric =
|
|
13
|
-
typeof value === "number"
|
|
14
|
-
? value
|
|
15
|
-
: typeof value === "string" && value.trim()
|
|
16
|
-
? Number(value)
|
|
17
|
-
: Number.NaN;
|
|
18
|
-
if (Number.isFinite(numeric)) return Math.max(0, Math.trunc(numeric));
|
|
19
|
-
}
|
|
20
|
-
return undefined;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function responsesErrorDetail(json: unknown, text: string, statusText: string): string {
|
|
24
|
-
const body = record(json);
|
|
25
|
-
if (body?.error !== undefined) {
|
|
26
|
-
return JSON.stringify(body.error).slice(0, 1000);
|
|
27
|
-
}
|
|
28
|
-
if (typeof body?.message === "string") {
|
|
29
|
-
return body.message.slice(0, 1000);
|
|
30
|
-
}
|
|
31
|
-
return text.slice(0, 1000) || statusText;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** Convert a Meta Responses API usage block into Pi's nested-tool usage shape. */
|
|
35
|
-
export function extractMetaResponseUsage(
|
|
36
|
-
response: unknown,
|
|
37
|
-
modelId = "muse-spark-1.2",
|
|
38
|
-
): Usage | undefined {
|
|
39
|
-
const usage = record(record(response)?.usage);
|
|
40
|
-
if (!usage) return undefined;
|
|
41
|
-
|
|
42
|
-
const inputDetails = record(
|
|
43
|
-
usage.input_tokens_details ?? usage.inputTokensDetails,
|
|
44
|
-
);
|
|
45
|
-
const outputDetails = record(
|
|
46
|
-
usage.output_tokens_details ?? usage.outputTokensDetails,
|
|
47
|
-
);
|
|
48
|
-
const inputTotal = tokenCount(
|
|
49
|
-
usage.input_tokens,
|
|
50
|
-
usage.inputTokens,
|
|
51
|
-
usage.prompt_tokens,
|
|
52
|
-
usage.promptTokens,
|
|
53
|
-
usage.input,
|
|
54
|
-
);
|
|
55
|
-
const output = tokenCount(
|
|
56
|
-
usage.output_tokens,
|
|
57
|
-
usage.outputTokens,
|
|
58
|
-
usage.completion_tokens,
|
|
59
|
-
usage.completionTokens,
|
|
60
|
-
usage.output,
|
|
61
|
-
);
|
|
62
|
-
if (inputTotal === undefined && output === undefined) return undefined;
|
|
63
|
-
|
|
64
|
-
const reportedInput = inputTotal ?? 0;
|
|
65
|
-
const cacheRead = Math.min(
|
|
66
|
-
reportedInput,
|
|
67
|
-
tokenCount(inputDetails?.cached_tokens, inputDetails?.cachedTokens) ?? 0,
|
|
68
|
-
);
|
|
69
|
-
const cacheWrite = Math.min(
|
|
70
|
-
reportedInput - cacheRead,
|
|
71
|
-
tokenCount(
|
|
72
|
-
inputDetails?.cache_write_tokens,
|
|
73
|
-
inputDetails?.cacheWriteTokens,
|
|
74
|
-
) ?? 0,
|
|
75
|
-
);
|
|
76
|
-
const uncachedInput = Math.max(0, reportedInput - cacheRead - cacheWrite);
|
|
77
|
-
const outputTokens = output ?? 0;
|
|
78
|
-
const reasoning = Math.min(
|
|
79
|
-
outputTokens,
|
|
80
|
-
tokenCount(
|
|
81
|
-
outputDetails?.reasoning_tokens,
|
|
82
|
-
outputDetails?.reasoningTokens,
|
|
83
|
-
) ?? 0,
|
|
84
|
-
);
|
|
85
|
-
const totalTokens =
|
|
86
|
-
tokenCount(usage.total_tokens, usage.totalTokens) ??
|
|
87
|
-
uncachedInput + cacheRead + cacheWrite + outputTokens;
|
|
88
|
-
const rates = metaFallbackCost(modelId);
|
|
89
|
-
const cost = {
|
|
90
|
-
input: (uncachedInput * (rates?.input ?? 0)) / 1_000_000,
|
|
91
|
-
output: (outputTokens * (rates?.output ?? 0)) / 1_000_000,
|
|
92
|
-
cacheRead: (cacheRead * (rates?.cacheRead ?? 0)) / 1_000_000,
|
|
93
|
-
cacheWrite: (cacheWrite * (rates?.cacheWrite ?? 0)) / 1_000_000,
|
|
94
|
-
total: 0,
|
|
95
|
-
};
|
|
96
|
-
cost.total = cost.input + cost.output + cost.cacheRead + cost.cacheWrite;
|
|
97
|
-
|
|
98
|
-
return {
|
|
99
|
-
input: uncachedInput,
|
|
100
|
-
output: outputTokens,
|
|
101
|
-
cacheRead,
|
|
102
|
-
cacheWrite,
|
|
103
|
-
reasoning,
|
|
104
|
-
totalTokens,
|
|
105
|
-
cost,
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export function formatMetaResponseUsage(usage: Usage): string {
|
|
110
|
-
const parts = [`${usage.input.toLocaleString()} input`];
|
|
111
|
-
if (usage.cacheRead)
|
|
112
|
-
parts.push(`${usage.cacheRead.toLocaleString()} cache read`);
|
|
113
|
-
if (usage.cacheWrite)
|
|
114
|
-
parts.push(`${usage.cacheWrite.toLocaleString()} cache write`);
|
|
115
|
-
parts.push(`${usage.output.toLocaleString()} output`);
|
|
116
|
-
if (usage.reasoning)
|
|
117
|
-
parts.push(`${usage.reasoning.toLocaleString()} reasoning`);
|
|
118
|
-
return `Video token usage: ${usage.totalTokens.toLocaleString()} total (${parts.join(", ")})`;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export function extractResponsesText(json: unknown): string {
|
|
122
|
-
if (!json || typeof json !== "object") return "";
|
|
123
|
-
const j = json as Record<string, unknown>;
|
|
124
|
-
if (typeof j.output_text === "string" && j.output_text.trim())
|
|
125
|
-
return j.output_text;
|
|
126
|
-
const output = j.output as unknown;
|
|
127
|
-
if (Array.isArray(output)) {
|
|
128
|
-
const texts: string[] = [];
|
|
129
|
-
for (const item of output as Record<string, unknown>[]) {
|
|
130
|
-
if (!item || typeof item !== "object" || item.type !== "message")
|
|
131
|
-
continue;
|
|
132
|
-
if (typeof item.content === "string" && item.content.trim())
|
|
133
|
-
texts.push(item.content);
|
|
134
|
-
if (Array.isArray(item.content)) {
|
|
135
|
-
for (const c of item.content as Record<string, unknown>[]) {
|
|
136
|
-
if (
|
|
137
|
-
(c.type === "output_text" || c.type === "text") &&
|
|
138
|
-
typeof c.text === "string" &&
|
|
139
|
-
c.text.trim()
|
|
140
|
-
)
|
|
141
|
-
texts.push(c.text);
|
|
142
|
-
if (
|
|
143
|
-
c.type === "refusal" &&
|
|
144
|
-
typeof c.refusal === "string" &&
|
|
145
|
-
c.refusal.trim()
|
|
146
|
-
)
|
|
147
|
-
texts.push(c.refusal);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
if (texts.length) return texts.join("\n\n");
|
|
152
|
-
}
|
|
153
|
-
if (typeof j.text === "string" && j.text.trim()) return j.text;
|
|
154
|
-
return "";
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export async function callMetaResponses(
|
|
158
|
-
apiKey: string,
|
|
159
|
-
payload: Record<string, unknown>,
|
|
160
|
-
signal?: AbortSignal,
|
|
161
|
-
): Promise<{ text: string; raw: unknown }> {
|
|
162
|
-
payload.store = false;
|
|
163
|
-
const res = await fetch(`${META_API_BASE_URL}/responses`, {
|
|
164
|
-
method: "POST",
|
|
165
|
-
headers: {
|
|
166
|
-
Authorization: `Bearer ${apiKey}`,
|
|
167
|
-
"Content-Type": "application/json",
|
|
168
|
-
"x-api-version": "1.0.0",
|
|
169
|
-
},
|
|
170
|
-
body: JSON.stringify(payload),
|
|
171
|
-
signal,
|
|
172
|
-
});
|
|
173
|
-
const text = await res.text();
|
|
174
|
-
let json: unknown;
|
|
175
|
-
try {
|
|
176
|
-
json = text ? (JSON.parse(text) as unknown) : {};
|
|
177
|
-
} catch {
|
|
178
|
-
json = text;
|
|
179
|
-
}
|
|
180
|
-
if (!res.ok) {
|
|
181
|
-
throw new Error(
|
|
182
|
-
`Meta Responses failed (HTTP ${res.status}): ${responsesErrorDetail(json, text, res.statusText)}`,
|
|
183
|
-
);
|
|
184
|
-
}
|
|
185
|
-
return { text: extractResponsesText(json), raw: json };
|
|
186
|
-
}
|