pi-meta-oauth 0.4.2 → 0.4.4
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 +40 -3
- package/extensions/media/files.ts +131 -0
- package/extensions/media/limits.ts +12 -0
- package/extensions/media/mime.ts +178 -0
- package/extensions/media/payload.ts +303 -0
- package/extensions/media/responses.ts +186 -0
- package/extensions/media-output.ts +127 -0
- package/extensions/media.ts +352 -1245
- package/extensions/meta.ts +181 -42
- package/extensions/voice/asr.ts +107 -0
- package/extensions/voice/helpers.ts +243 -0
- package/extensions/voice/macos-audio.swift +6 -16
- package/extensions/voice.ts +27 -333
- package/package.json +1 -1
|
@@ -0,0 +1,186 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_MEDIA_MAX_CHARS = 20_000;
|
|
7
|
+
export const MAX_MEDIA_MAX_CHARS = 50_000;
|
|
8
|
+
export const DEFAULT_MEDIA_MAX_OUTPUT_TOKENS = 8_000;
|
|
9
|
+
export const MIN_MEDIA_MAX_OUTPUT_TOKENS = 4_000;
|
|
10
|
+
export const MAX_MEDIA_MAX_OUTPUT_TOKENS = 32_000;
|
|
11
|
+
const MAX_INLINE_BYTES = 50 * 1024;
|
|
12
|
+
const MAX_INLINE_LINES = 2_000;
|
|
13
|
+
|
|
14
|
+
export interface MediaOutputDetails {
|
|
15
|
+
path?: string;
|
|
16
|
+
chars: number;
|
|
17
|
+
lines: number;
|
|
18
|
+
truncated: boolean;
|
|
19
|
+
incomplete: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function clampInteger(value: number | undefined, fallback: number, minimum: number, maximum: number): number {
|
|
23
|
+
if (value === undefined) return fallback;
|
|
24
|
+
return Math.max(minimum, Math.min(maximum, Math.trunc(value)));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function mediaMaxChars(value: number | undefined): number {
|
|
28
|
+
return clampInteger(value, DEFAULT_MEDIA_MAX_CHARS, 1_000, MAX_MEDIA_MAX_CHARS);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function mediaMaxOutputTokens(value: number | undefined): number {
|
|
32
|
+
return clampInteger(
|
|
33
|
+
value,
|
|
34
|
+
DEFAULT_MEDIA_MAX_OUTPUT_TOKENS,
|
|
35
|
+
MIN_MEDIA_MAX_OUTPUT_TOKENS,
|
|
36
|
+
MAX_MEDIA_MAX_OUTPUT_TOKENS,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function inlineSlice(text: string, maxChars: number): string {
|
|
41
|
+
let slice = text.split("\n").slice(0, MAX_INLINE_LINES).join("\n").slice(0, maxChars);
|
|
42
|
+
if (Buffer.byteLength(slice, "utf8") <= MAX_INLINE_BYTES) return slice;
|
|
43
|
+
const bytes = Buffer.from(slice, "utf8").subarray(0, MAX_INLINE_BYTES);
|
|
44
|
+
slice = new TextDecoder().decode(bytes);
|
|
45
|
+
return slice.endsWith("\uFFFD") ? slice.slice(0, -1) : slice;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function responseReason(response: unknown): string | undefined {
|
|
49
|
+
if (!response || typeof response !== "object") return undefined;
|
|
50
|
+
const record = response as Record<string, unknown>;
|
|
51
|
+
const details = record.incomplete_details;
|
|
52
|
+
if (details && typeof details === "object") {
|
|
53
|
+
const reason = (details as Record<string, unknown>).reason;
|
|
54
|
+
if (typeof reason === "string") return reason;
|
|
55
|
+
}
|
|
56
|
+
for (const key of ["finish_reason", "stop_reason"]) {
|
|
57
|
+
if (typeof record[key] === "string") return record[key] as string;
|
|
58
|
+
}
|
|
59
|
+
return record.status === "incomplete" ? "incomplete" : undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function responseHitOutputLimit(response: unknown): boolean {
|
|
63
|
+
const reason = responseReason(response)?.toLowerCase();
|
|
64
|
+
return reason === "max_output_tokens" || reason === "length" || reason === "max_tokens";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function outputFilePath(identity: string, text: string): string {
|
|
68
|
+
const hash = createHash("sha256").update(`${identity}\0${text.length}\0${text.slice(0, 1024)}`).digest("hex").slice(0, 16);
|
|
69
|
+
return join(tmpdir(), `pi-meta-media-${hash}.txt`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function saveFullOutput(identity: string, text: string): Promise<string | undefined> {
|
|
73
|
+
const basePath = outputFilePath(identity, text);
|
|
74
|
+
try {
|
|
75
|
+
await mkdir(tmpdir(), { recursive: true });
|
|
76
|
+
try {
|
|
77
|
+
await writeFile(basePath, text, { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
78
|
+
return basePath;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
81
|
+
const uniquePath = `${basePath}.${randomBytes(4).toString("hex")}`;
|
|
82
|
+
await writeFile(uniquePath, text, { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
83
|
+
return uniquePath;
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function prepareMediaOutput(options: {
|
|
91
|
+
text: string;
|
|
92
|
+
identity: string;
|
|
93
|
+
maxChars?: number;
|
|
94
|
+
response?: unknown;
|
|
95
|
+
}): Promise<{ text: string; details: MediaOutputDetails }> {
|
|
96
|
+
const { text, identity, response } = options;
|
|
97
|
+
const slice = inlineSlice(text, mediaMaxChars(options.maxChars));
|
|
98
|
+
const truncated = slice.length < text.length;
|
|
99
|
+
const lines = text.length === 0 ? 0 : text.split("\n").length;
|
|
100
|
+
const incomplete = responseHitOutputLimit(response);
|
|
101
|
+
const savedPath = truncated ? await saveFullOutput(identity, text) : undefined;
|
|
102
|
+
const notes: string[] = [];
|
|
103
|
+
if (truncated) {
|
|
104
|
+
const continuation =
|
|
105
|
+
lines <= 1
|
|
106
|
+
? `full content saved to: ${savedPath}. Open that file directly; it is a single line so read offset/limit cannot page through it.`
|
|
107
|
+
: `full content saved to: ${savedPath} (${lines} lines). Use the read tool with offset/limit to continue (offset is a 1-based line number).`;
|
|
108
|
+
notes.push(
|
|
109
|
+
savedPath
|
|
110
|
+
? `content truncated at ${slice.length} of ${text.length} characters; ${continuation}`
|
|
111
|
+
: `content truncated at ${slice.length} of ${text.length} characters; the full content could not be saved, so continuation is unavailable.`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
if (incomplete) {
|
|
115
|
+
notes.push("Muse exhausted max_output_tokens, so the analysis may be incomplete. Retry with a larger budget or a narrower prompt.");
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
text: `${slice}${notes.length > 0 ? `\n\n[${notes.join(" ")}]` : ""}`,
|
|
119
|
+
details: {
|
|
120
|
+
...(savedPath ? { path: savedPath } : {}),
|
|
121
|
+
chars: text.length,
|
|
122
|
+
lines,
|
|
123
|
+
truncated,
|
|
124
|
+
incomplete,
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|