@steipete/summarize 0.7.1 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -0
- package/README.md +53 -2
- package/dist/cli.js +3 -0
- package/dist/esm/cache.js +353 -0
- package/dist/esm/cache.js.map +1 -0
- package/dist/esm/config.js +78 -1
- package/dist/esm/config.js.map +1 -1
- package/dist/esm/content/asset.js +11 -17
- package/dist/esm/content/asset.js.map +1 -1
- package/dist/esm/daemon/auto-mode.js +8 -0
- package/dist/esm/daemon/auto-mode.js.map +1 -0
- package/dist/esm/daemon/cli.js +284 -0
- package/dist/esm/daemon/cli.js.map +1 -0
- package/dist/esm/daemon/config.js +82 -0
- package/dist/esm/daemon/config.js.map +1 -0
- package/dist/esm/daemon/constants.js +8 -0
- package/dist/esm/daemon/constants.js.map +1 -0
- package/dist/esm/daemon/env-merge.js +4 -0
- package/dist/esm/daemon/env-merge.js.map +1 -0
- package/dist/esm/daemon/env-snapshot.js +43 -0
- package/dist/esm/daemon/env-snapshot.js.map +1 -0
- package/dist/esm/daemon/flow-context.js +265 -0
- package/dist/esm/daemon/flow-context.js.map +1 -0
- package/dist/esm/daemon/launchd.js +149 -0
- package/dist/esm/daemon/launchd.js.map +1 -0
- package/dist/esm/daemon/meta.js +35 -0
- package/dist/esm/daemon/meta.js.map +1 -0
- package/dist/esm/daemon/models.js +175 -0
- package/dist/esm/daemon/models.js.map +1 -0
- package/dist/esm/daemon/request-settings.js +91 -0
- package/dist/esm/daemon/request-settings.js.map +1 -0
- package/dist/esm/daemon/schtasks.js +108 -0
- package/dist/esm/daemon/schtasks.js.map +1 -0
- package/dist/esm/daemon/server.js +399 -0
- package/dist/esm/daemon/server.js.map +1 -0
- package/dist/esm/daemon/summarize-progress.js +57 -0
- package/dist/esm/daemon/summarize-progress.js.map +1 -0
- package/dist/esm/daemon/summarize.js +263 -0
- package/dist/esm/daemon/summarize.js.map +1 -0
- package/dist/esm/daemon/systemd.js +117 -0
- package/dist/esm/daemon/systemd.js.map +1 -0
- package/dist/esm/flags.js +3 -1
- package/dist/esm/flags.js.map +1 -1
- package/dist/esm/llm/generate-text.js +445 -154
- package/dist/esm/llm/generate-text.js.map +1 -1
- package/dist/esm/llm/html-to-markdown.js +4 -1
- package/dist/esm/llm/html-to-markdown.js.map +1 -1
- package/dist/esm/llm/prompt.js +14 -0
- package/dist/esm/llm/prompt.js.map +1 -0
- package/dist/esm/llm/transcript-to-markdown.js +57 -0
- package/dist/esm/llm/transcript-to-markdown.js.map +1 -0
- package/dist/esm/model-spec.js +2 -2
- package/dist/esm/model-spec.js.map +1 -1
- package/dist/esm/run/attachments.js +10 -42
- package/dist/esm/run/attachments.js.map +1 -1
- package/dist/esm/run/cache-state.js +48 -0
- package/dist/esm/run/cache-state.js.map +1 -0
- package/dist/esm/run/cli-preflight.js +15 -1
- package/dist/esm/run/cli-preflight.js.map +1 -1
- package/dist/esm/run/cookies/twitter.js +224 -0
- package/dist/esm/run/cookies/twitter.js.map +1 -0
- package/dist/esm/run/fetch-with-timeout.js +1 -1
- package/dist/esm/run/fetch-with-timeout.js.map +1 -1
- package/dist/esm/run/finish-line.js +46 -17
- package/dist/esm/run/finish-line.js.map +1 -1
- package/dist/esm/run/flows/asset/input.js +2 -4
- package/dist/esm/run/flows/asset/input.js.map +1 -1
- package/dist/esm/run/flows/asset/preprocess.js +52 -72
- package/dist/esm/run/flows/asset/preprocess.js.map +1 -1
- package/dist/esm/run/flows/asset/summary.js +127 -47
- package/dist/esm/run/flows/asset/summary.js.map +1 -1
- package/dist/esm/run/flows/url/extract.js +6 -1
- package/dist/esm/run/flows/url/extract.js.map +1 -1
- package/dist/esm/run/flows/url/flow.js +166 -85
- package/dist/esm/run/flows/url/flow.js.map +1 -1
- package/dist/esm/run/flows/url/markdown.js +88 -46
- package/dist/esm/run/flows/url/markdown.js.map +1 -1
- package/dist/esm/run/flows/url/summary.js +263 -185
- package/dist/esm/run/flows/url/summary.js.map +1 -1
- package/dist/esm/run/help.js +33 -2
- package/dist/esm/run/help.js.map +1 -1
- package/dist/esm/run/run-env.js +36 -2
- package/dist/esm/run/run-env.js.map +1 -1
- package/dist/esm/run/runner.js +362 -227
- package/dist/esm/run/runner.js.map +1 -1
- package/dist/esm/run/summary-engine.js +21 -6
- package/dist/esm/run/summary-engine.js.map +1 -1
- package/dist/esm/run/summary-llm.js +4 -1
- package/dist/esm/run/summary-llm.js.map +1 -1
- package/dist/esm/tty/format.js +9 -0
- package/dist/esm/tty/format.js.map +1 -1
- package/dist/esm/version.js +1 -1
- package/dist/types/cache.d.ts +70 -0
- package/dist/types/config.d.ts +46 -0
- package/dist/types/content/asset.d.ts +4 -3
- package/dist/types/daemon/auto-mode.d.ts +8 -0
- package/dist/types/daemon/cli.d.ts +9 -0
- package/dist/types/daemon/config.d.ts +19 -0
- package/dist/types/daemon/constants.d.ts +7 -0
- package/dist/types/daemon/env-merge.d.ts +5 -0
- package/dist/types/daemon/env-snapshot.d.ts +4 -0
- package/dist/types/daemon/flow-context.d.ts +28 -0
- package/dist/types/daemon/launchd.d.ts +29 -0
- package/dist/types/daemon/meta.d.ts +12 -0
- package/dist/types/daemon/models.d.ts +27 -0
- package/dist/types/daemon/request-settings.d.ts +27 -0
- package/dist/types/daemon/schtasks.d.ts +16 -0
- package/dist/types/daemon/server.d.ts +12 -0
- package/dist/types/daemon/summarize-progress.d.ts +2 -0
- package/dist/types/daemon/summarize.d.ts +59 -0
- package/dist/types/daemon/systemd.d.ts +16 -0
- package/dist/types/flags.d.ts +1 -1
- package/dist/types/llm/generate-text.d.ts +11 -5
- package/dist/types/llm/html-to-markdown.d.ts +4 -1
- package/dist/types/llm/prompt.d.ts +9 -0
- package/dist/types/llm/transcript-to-markdown.d.ts +34 -0
- package/dist/types/run/attachments.d.ts +4 -10
- package/dist/types/run/cache-state.d.ts +12 -0
- package/dist/types/run/cli-preflight.d.ts +1 -0
- package/dist/types/run/cookies/twitter.d.ts +17 -0
- package/dist/types/run/finish-line.d.ts +31 -1
- package/dist/types/run/flows/asset/preprocess.d.ts +5 -2
- package/dist/types/run/flows/asset/summary.d.ts +11 -0
- package/dist/types/run/flows/url/markdown.d.ts +3 -0
- package/dist/types/run/flows/url/summary.d.ts +6 -3
- package/dist/types/run/flows/url/types.d.ts +52 -18
- package/dist/types/run/help.d.ts +1 -0
- package/dist/types/run/run-env.d.ts +6 -0
- package/dist/types/run/summary-engine.d.ts +8 -2
- package/dist/types/run/summary-llm.d.ts +6 -3
- package/dist/types/tty/format.d.ts +1 -0
- package/dist/types/version.d.ts +1 -1
- package/docs/README.md +5 -0
- package/docs/cache.md +72 -0
- package/docs/chrome-extension.md +180 -0
- package/docs/cli.md +6 -0
- package/docs/config.md +65 -1
- package/docs/extract-only.md +6 -0
- package/docs/firecrawl.md +6 -0
- package/docs/language.md +6 -0
- package/docs/llm.md +20 -0
- package/docs/manual-tests.md +6 -0
- package/docs/model-auto.md +6 -0
- package/docs/openai.md +6 -0
- package/docs/site/index.html +11 -1
- package/docs/smoketest.md +6 -0
- package/docs/website.md +6 -0
- package/docs/youtube.md +9 -2
- package/package.json +7 -10
- package/dist/cli.cjs +0 -80566
- package/dist/cli.cjs.map +0 -7
|
@@ -1,9 +1,5 @@
|
|
|
1
|
+
import { completeSimple, getModel, streamSimple } from '@mariozechner/pi-ai';
|
|
1
2
|
import { parseGatewayStyleModelId } from './model-id.js';
|
|
2
|
-
function assertNonEmptyText(text, modelId) {
|
|
3
|
-
if (text.trim().length > 0)
|
|
4
|
-
return;
|
|
5
|
-
throw new Error(`LLM returned an empty summary (model ${modelId}).`);
|
|
6
|
-
}
|
|
7
3
|
function parseAnthropicErrorPayload(responseBody) {
|
|
8
4
|
try {
|
|
9
5
|
const parsed = JSON.parse(responseBody);
|
|
@@ -48,25 +44,20 @@ function normalizeTokenUsage(raw) {
|
|
|
48
44
|
if (!raw || typeof raw !== 'object')
|
|
49
45
|
return null;
|
|
50
46
|
const usage = raw;
|
|
51
|
-
const promptTokens = typeof usage.
|
|
52
|
-
|
|
53
|
-
: typeof usage.inputTokens === 'number' && Number.isFinite(usage.inputTokens)
|
|
54
|
-
? usage.inputTokens
|
|
55
|
-
: null;
|
|
56
|
-
const completionTokens = typeof usage.completionTokens === 'number' && Number.isFinite(usage.completionTokens)
|
|
57
|
-
? usage.completionTokens
|
|
58
|
-
: typeof usage.outputTokens === 'number' && Number.isFinite(usage.outputTokens)
|
|
59
|
-
? usage.outputTokens
|
|
60
|
-
: null;
|
|
47
|
+
const promptTokens = typeof usage.input === 'number' && Number.isFinite(usage.input) ? usage.input : null;
|
|
48
|
+
const completionTokens = typeof usage.output === 'number' && Number.isFinite(usage.output) ? usage.output : null;
|
|
61
49
|
const totalTokens = typeof usage.totalTokens === 'number' && Number.isFinite(usage.totalTokens)
|
|
62
50
|
? usage.totalTokens
|
|
63
51
|
: null;
|
|
64
|
-
if (promptTokens === null && completionTokens === null && totalTokens === null)
|
|
52
|
+
if (promptTokens === null && completionTokens === null && totalTokens === null)
|
|
65
53
|
return null;
|
|
66
|
-
}
|
|
67
54
|
return { promptTokens, completionTokens, totalTokens };
|
|
68
55
|
}
|
|
69
|
-
function
|
|
56
|
+
function resolveBaseUrlOverride(raw) {
|
|
57
|
+
const trimmed = typeof raw === 'string' ? raw.trim() : '';
|
|
58
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
59
|
+
}
|
|
60
|
+
function resolveOpenAiClientConfig({ apiKeys, forceOpenRouter, openaiBaseUrlOverride, forceChatCompletions, }) {
|
|
70
61
|
const baseUrlRaw = openaiBaseUrlOverride ??
|
|
71
62
|
(typeof process !== 'undefined' ? process.env.OPENAI_BASE_URL : undefined);
|
|
72
63
|
const baseUrl = typeof baseUrlRaw === 'string' && baseUrlRaw.trim().length > 0 ? baseUrlRaw.trim() : null;
|
|
@@ -84,53 +75,221 @@ function resolveOpenAiClientConfig({ apiKeys, fetchImpl, forceOpenRouter, openai
|
|
|
84
75
|
? 'Missing OPENROUTER_API_KEY (or OPENAI_API_KEY) for OpenRouter'
|
|
85
76
|
: 'Missing OPENAI_API_KEY for openai/... model');
|
|
86
77
|
}
|
|
87
|
-
const wrappedFetch = isOpenRouter
|
|
88
|
-
? (url, init) => {
|
|
89
|
-
const headers = new Headers(init?.headers);
|
|
90
|
-
headers.set('HTTP-Referer', 'https://github.com/steipete/summarize');
|
|
91
|
-
headers.set('X-Title', 'summarize');
|
|
92
|
-
return fetchImpl(url, { ...init, headers });
|
|
93
|
-
}
|
|
94
|
-
: fetchImpl;
|
|
95
78
|
const baseURL = forceOpenRouter
|
|
96
79
|
? 'https://openrouter.ai/api/v1'
|
|
97
80
|
: (baseUrl ?? (isOpenRouter ? 'https://openrouter.ai/api/v1' : undefined));
|
|
98
|
-
const
|
|
81
|
+
const isCustomBaseURL = (() => {
|
|
82
|
+
if (!baseURL)
|
|
83
|
+
return false;
|
|
84
|
+
try {
|
|
85
|
+
const url = new URL(baseURL);
|
|
86
|
+
return url.host !== 'api.openai.com' && url.host !== 'openrouter.ai';
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
})();
|
|
92
|
+
const useChatCompletions = Boolean(forceChatCompletions) || isOpenRouter || isCustomBaseURL;
|
|
99
93
|
return {
|
|
100
94
|
apiKey,
|
|
101
95
|
baseURL: baseURL ?? undefined,
|
|
102
|
-
fetch: wrappedFetch,
|
|
103
96
|
useChatCompletions,
|
|
104
97
|
isOpenRouter,
|
|
105
98
|
};
|
|
106
99
|
}
|
|
107
|
-
|
|
100
|
+
function promptToContext({ system, prompt }) {
|
|
101
|
+
const messages = typeof prompt === 'string'
|
|
102
|
+
? [{ role: 'user', content: prompt, timestamp: Date.now() }]
|
|
103
|
+
: prompt.map((msg) => typeof msg.timestamp === 'number'
|
|
104
|
+
? msg
|
|
105
|
+
: { ...msg, timestamp: Date.now() });
|
|
106
|
+
return { systemPrompt: system, messages };
|
|
107
|
+
}
|
|
108
|
+
function extractText(message) {
|
|
109
|
+
const text = message.content
|
|
110
|
+
.filter((c) => c.type === 'text')
|
|
111
|
+
.map((c) => c.text)
|
|
112
|
+
.join('');
|
|
113
|
+
return text.trim();
|
|
114
|
+
}
|
|
115
|
+
function wantsImages(context) {
|
|
116
|
+
for (const msg of context.messages) {
|
|
117
|
+
if (msg.role === 'user' || msg.role === 'toolResult') {
|
|
118
|
+
if (Array.isArray(msg.content) && msg.content.some((c) => c.type === 'image'))
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
function tryGetModel(provider, modelId) {
|
|
125
|
+
try {
|
|
126
|
+
return getModel(provider, modelId);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function createSyntheticModel({ provider, modelId, api, baseUrl, allowImages, headers, }) {
|
|
133
|
+
return {
|
|
134
|
+
id: modelId,
|
|
135
|
+
name: `${provider}/${modelId}`,
|
|
136
|
+
api,
|
|
137
|
+
provider,
|
|
138
|
+
baseUrl,
|
|
139
|
+
reasoning: false,
|
|
140
|
+
input: allowImages ? ['text', 'image'] : ['text'],
|
|
141
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
142
|
+
contextWindow: 128_000,
|
|
143
|
+
maxTokens: 16_384,
|
|
144
|
+
...(headers ? { headers } : {}),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function resolveModelForCall({ modelId, parsedProvider, openaiConfig, context, openaiBaseUrlOverride, anthropicBaseUrlOverride, googleBaseUrlOverride, xaiBaseUrlOverride, }) {
|
|
148
|
+
const allowImages = wantsImages(context);
|
|
149
|
+
if (parsedProvider === 'openai') {
|
|
150
|
+
const base = tryGetModel('openai', modelId);
|
|
151
|
+
const api = openaiConfig?.useChatCompletions ? 'openai-completions' : 'openai-responses';
|
|
152
|
+
const baseUrl = openaiConfig?.baseURL ?? base?.baseUrl ?? 'https://api.openai.com/v1';
|
|
153
|
+
const headers = openaiConfig?.isOpenRouter
|
|
154
|
+
? {
|
|
155
|
+
...(base?.headers ?? {}),
|
|
156
|
+
'HTTP-Referer': 'https://github.com/steipete/summarize',
|
|
157
|
+
'X-Title': 'summarize',
|
|
158
|
+
}
|
|
159
|
+
: base?.headers;
|
|
160
|
+
return {
|
|
161
|
+
...(base ?? createSyntheticModel({ provider: 'openai', modelId, api, baseUrl, allowImages })),
|
|
162
|
+
api,
|
|
163
|
+
baseUrl,
|
|
164
|
+
...(headers ? { headers } : {}),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
if (parsedProvider === 'zai') {
|
|
168
|
+
const base = tryGetModel('zai', modelId);
|
|
169
|
+
const api = 'openai-completions';
|
|
170
|
+
const baseUrl = openaiBaseUrlOverride ??
|
|
171
|
+
base?.baseUrl ??
|
|
172
|
+
openaiConfig?.baseURL ??
|
|
173
|
+
'https://api.z.ai/api/paas/v4';
|
|
174
|
+
return {
|
|
175
|
+
...(base ?? createSyntheticModel({ provider: 'zai', modelId, api, baseUrl, allowImages })),
|
|
176
|
+
api,
|
|
177
|
+
baseUrl,
|
|
178
|
+
input: allowImages ? ['text', 'image'] : ['text'],
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
if (parsedProvider === 'xai') {
|
|
182
|
+
const base = tryGetModel('xai', modelId);
|
|
183
|
+
const override = resolveBaseUrlOverride(xaiBaseUrlOverride);
|
|
184
|
+
if (override) {
|
|
185
|
+
return {
|
|
186
|
+
...(base ??
|
|
187
|
+
createSyntheticModel({
|
|
188
|
+
provider: 'xai',
|
|
189
|
+
modelId,
|
|
190
|
+
api: 'openai-completions',
|
|
191
|
+
baseUrl: override,
|
|
192
|
+
allowImages,
|
|
193
|
+
})),
|
|
194
|
+
baseUrl: override,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
return (base ??
|
|
198
|
+
createSyntheticModel({
|
|
199
|
+
provider: 'xai',
|
|
200
|
+
modelId,
|
|
201
|
+
api: 'openai-completions',
|
|
202
|
+
baseUrl: 'https://api.x.ai/v1',
|
|
203
|
+
allowImages,
|
|
204
|
+
}));
|
|
205
|
+
}
|
|
206
|
+
if (parsedProvider === 'google') {
|
|
207
|
+
const base = tryGetModel('google', modelId);
|
|
208
|
+
const override = resolveBaseUrlOverride(googleBaseUrlOverride);
|
|
209
|
+
if (override) {
|
|
210
|
+
return {
|
|
211
|
+
...(base ??
|
|
212
|
+
createSyntheticModel({
|
|
213
|
+
provider: 'google',
|
|
214
|
+
modelId,
|
|
215
|
+
api: 'google-generative-ai',
|
|
216
|
+
baseUrl: override,
|
|
217
|
+
allowImages,
|
|
218
|
+
})),
|
|
219
|
+
baseUrl: override,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
return (base ??
|
|
223
|
+
createSyntheticModel({
|
|
224
|
+
provider: 'google',
|
|
225
|
+
modelId,
|
|
226
|
+
api: 'google-generative-ai',
|
|
227
|
+
baseUrl: 'https://generativelanguage.googleapis.com/v1beta',
|
|
228
|
+
allowImages,
|
|
229
|
+
}));
|
|
230
|
+
}
|
|
231
|
+
const base = tryGetModel('anthropic', modelId);
|
|
232
|
+
const override = resolveBaseUrlOverride(anthropicBaseUrlOverride);
|
|
233
|
+
if (override) {
|
|
234
|
+
return {
|
|
235
|
+
...(base ??
|
|
236
|
+
createSyntheticModel({
|
|
237
|
+
provider: 'anthropic',
|
|
238
|
+
modelId,
|
|
239
|
+
api: 'anthropic-messages',
|
|
240
|
+
baseUrl: override,
|
|
241
|
+
allowImages,
|
|
242
|
+
})),
|
|
243
|
+
baseUrl: override,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
return (base ??
|
|
247
|
+
createSyntheticModel({
|
|
248
|
+
provider: 'anthropic',
|
|
249
|
+
modelId,
|
|
250
|
+
api: 'anthropic-messages',
|
|
251
|
+
baseUrl: 'https://api.anthropic.com',
|
|
252
|
+
allowImages,
|
|
253
|
+
}));
|
|
254
|
+
}
|
|
255
|
+
export async function generateTextWithModelId({ modelId, apiKeys, system, prompt, temperature, maxOutputTokens, timeoutMs, fetchImpl: _fetchImpl, forceOpenRouter, openaiBaseUrlOverride, anthropicBaseUrlOverride, googleBaseUrlOverride, xaiBaseUrlOverride, forceChatCompletions, retries = 0, onRetry, }) {
|
|
256
|
+
void _fetchImpl;
|
|
108
257
|
const parsed = parseGatewayStyleModelId(modelId);
|
|
258
|
+
const context = promptToContext({ system, prompt });
|
|
259
|
+
const isOpenaiGpt5 = parsed.provider === 'openai' && /^gpt-5([-.].+)?$/i.test(parsed.model);
|
|
260
|
+
const effectiveTemperature = typeof temperature === 'number' && !(isOpenaiGpt5 && temperature === 0)
|
|
261
|
+
? temperature
|
|
262
|
+
: undefined;
|
|
109
263
|
const maxRetries = Math.max(0, retries);
|
|
110
264
|
let attempt = 0;
|
|
111
265
|
while (attempt <= maxRetries) {
|
|
112
266
|
const controller = new AbortController();
|
|
113
267
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
114
268
|
try {
|
|
115
|
-
const { generateText } = await import('ai');
|
|
116
|
-
const shouldSendMaxOutputTokens = () => typeof maxOutputTokens === 'number';
|
|
117
269
|
if (parsed.provider === 'xai') {
|
|
118
270
|
const apiKey = apiKeys.xaiApiKey;
|
|
119
271
|
if (!apiKey)
|
|
120
272
|
throw new Error('Missing XAI_API_KEY for xai/... model');
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
273
|
+
const model = resolveModelForCall({
|
|
274
|
+
modelId: parsed.model,
|
|
275
|
+
parsedProvider: parsed.provider,
|
|
276
|
+
openaiConfig: null,
|
|
277
|
+
context,
|
|
278
|
+
xaiBaseUrlOverride,
|
|
279
|
+
});
|
|
280
|
+
const result = await completeSimple(model, context, {
|
|
281
|
+
...(typeof effectiveTemperature === 'number'
|
|
282
|
+
? { temperature: effectiveTemperature }
|
|
283
|
+
: {}),
|
|
284
|
+
...(typeof maxOutputTokens === 'number' ? { maxTokens: maxOutputTokens } : {}),
|
|
285
|
+
apiKey,
|
|
286
|
+
signal: controller.signal,
|
|
130
287
|
});
|
|
131
|
-
|
|
288
|
+
const text = extractText(result);
|
|
289
|
+
if (!text)
|
|
290
|
+
throw new Error(`LLM returned an empty summary (model ${parsed.canonical}).`);
|
|
132
291
|
return {
|
|
133
|
-
text
|
|
292
|
+
text,
|
|
134
293
|
canonicalModelId: parsed.canonical,
|
|
135
294
|
provider: parsed.provider,
|
|
136
295
|
usage: normalizeTokenUsage(result.usage),
|
|
@@ -140,19 +299,26 @@ export async function generateTextWithModelId({ modelId, apiKeys, system, prompt
|
|
|
140
299
|
const apiKey = apiKeys.googleApiKey;
|
|
141
300
|
if (!apiKey)
|
|
142
301
|
throw new Error('Missing GEMINI_API_KEY (or GOOGLE_GENERATIVE_AI_API_KEY / GOOGLE_API_KEY) for google/... model');
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
302
|
+
const model = resolveModelForCall({
|
|
303
|
+
modelId: parsed.model,
|
|
304
|
+
parsedProvider: parsed.provider,
|
|
305
|
+
openaiConfig: null,
|
|
306
|
+
context,
|
|
307
|
+
googleBaseUrlOverride,
|
|
308
|
+
});
|
|
309
|
+
const result = await completeSimple(model, context, {
|
|
310
|
+
...(typeof effectiveTemperature === 'number'
|
|
311
|
+
? { temperature: effectiveTemperature }
|
|
312
|
+
: {}),
|
|
313
|
+
...(typeof maxOutputTokens === 'number' ? { maxTokens: maxOutputTokens } : {}),
|
|
314
|
+
apiKey,
|
|
315
|
+
signal: controller.signal,
|
|
152
316
|
});
|
|
153
|
-
|
|
317
|
+
const text = extractText(result);
|
|
318
|
+
if (!text)
|
|
319
|
+
throw new Error(`LLM returned an empty summary (model ${parsed.canonical}).`);
|
|
154
320
|
return {
|
|
155
|
-
text
|
|
321
|
+
text,
|
|
156
322
|
canonicalModelId: parsed.canonical,
|
|
157
323
|
provider: parsed.provider,
|
|
158
324
|
usage: normalizeTokenUsage(result.usage),
|
|
@@ -162,52 +328,89 @@ export async function generateTextWithModelId({ modelId, apiKeys, system, prompt
|
|
|
162
328
|
const apiKey = apiKeys.anthropicApiKey;
|
|
163
329
|
if (!apiKey)
|
|
164
330
|
throw new Error('Missing ANTHROPIC_API_KEY for anthropic/... model');
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
...(typeof temperature === 'number' ? { temperature } : {}),
|
|
172
|
-
...(shouldSendMaxOutputTokens() ? { maxOutputTokens } : {}),
|
|
173
|
-
abortSignal: controller.signal,
|
|
331
|
+
const model = resolveModelForCall({
|
|
332
|
+
modelId: parsed.model,
|
|
333
|
+
parsedProvider: parsed.provider,
|
|
334
|
+
openaiConfig: null,
|
|
335
|
+
context,
|
|
336
|
+
anthropicBaseUrlOverride,
|
|
174
337
|
});
|
|
175
|
-
|
|
338
|
+
const result = await completeSimple(model, context, {
|
|
339
|
+
...(typeof effectiveTemperature === 'number'
|
|
340
|
+
? { temperature: effectiveTemperature }
|
|
341
|
+
: {}),
|
|
342
|
+
...(typeof maxOutputTokens === 'number' ? { maxTokens: maxOutputTokens } : {}),
|
|
343
|
+
apiKey,
|
|
344
|
+
signal: controller.signal,
|
|
345
|
+
});
|
|
346
|
+
const text = extractText(result);
|
|
347
|
+
if (!text)
|
|
348
|
+
throw new Error(`LLM returned an empty summary (model ${parsed.canonical}).`);
|
|
176
349
|
return {
|
|
177
|
-
text
|
|
350
|
+
text,
|
|
178
351
|
canonicalModelId: parsed.canonical,
|
|
179
352
|
provider: parsed.provider,
|
|
180
353
|
usage: normalizeTokenUsage(result.usage),
|
|
181
354
|
};
|
|
182
355
|
}
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
356
|
+
const openaiConfig = parsed.provider === 'openai'
|
|
357
|
+
? resolveOpenAiClientConfig({
|
|
358
|
+
apiKeys,
|
|
359
|
+
forceOpenRouter,
|
|
360
|
+
openaiBaseUrlOverride,
|
|
361
|
+
forceChatCompletions,
|
|
362
|
+
})
|
|
363
|
+
: null;
|
|
364
|
+
if (parsed.provider === 'zai') {
|
|
365
|
+
const apiKey = apiKeys.openaiApiKey;
|
|
366
|
+
if (!apiKey)
|
|
367
|
+
throw new Error('Missing Z_AI_API_KEY for zai/... model');
|
|
368
|
+
const model = resolveModelForCall({
|
|
369
|
+
modelId: parsed.model,
|
|
370
|
+
parsedProvider: parsed.provider,
|
|
371
|
+
openaiConfig: null,
|
|
372
|
+
context,
|
|
373
|
+
openaiBaseUrlOverride,
|
|
374
|
+
});
|
|
375
|
+
const result = await completeSimple(model, context, {
|
|
376
|
+
...(typeof effectiveTemperature === 'number'
|
|
377
|
+
? { temperature: effectiveTemperature }
|
|
378
|
+
: {}),
|
|
379
|
+
...(typeof maxOutputTokens === 'number' ? { maxTokens: maxOutputTokens } : {}),
|
|
380
|
+
apiKey,
|
|
381
|
+
signal: controller.signal,
|
|
382
|
+
});
|
|
383
|
+
const text = extractText(result);
|
|
384
|
+
if (!text)
|
|
385
|
+
throw new Error(`LLM returned an empty summary (model ${parsed.canonical}).`);
|
|
386
|
+
return {
|
|
387
|
+
text,
|
|
388
|
+
canonicalModelId: parsed.canonical,
|
|
389
|
+
provider: parsed.provider,
|
|
390
|
+
usage: normalizeTokenUsage(result.usage),
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
const model = resolveModelForCall({
|
|
394
|
+
modelId: parsed.model,
|
|
395
|
+
parsedProvider: parsed.provider,
|
|
396
|
+
openaiConfig,
|
|
397
|
+
context,
|
|
188
398
|
openaiBaseUrlOverride,
|
|
189
|
-
|
|
399
|
+
anthropicBaseUrlOverride,
|
|
400
|
+
googleBaseUrlOverride,
|
|
401
|
+
xaiBaseUrlOverride,
|
|
190
402
|
});
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
...(
|
|
194
|
-
|
|
403
|
+
const result = await completeSimple(model, context, {
|
|
404
|
+
...(typeof effectiveTemperature === 'number' ? { temperature: effectiveTemperature } : {}),
|
|
405
|
+
...(typeof maxOutputTokens === 'number' ? { maxTokens: maxOutputTokens } : {}),
|
|
406
|
+
apiKey: openaiConfig?.apiKey ?? apiKeys.openaiApiKey ?? undefined,
|
|
407
|
+
signal: controller.signal,
|
|
195
408
|
});
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const chatModelId = parsed.model;
|
|
200
|
-
const result = await generateText({
|
|
201
|
-
model: useChatCompletions ? openai.chat(chatModelId) : openai(responsesModelId),
|
|
202
|
-
system,
|
|
203
|
-
...(typeof prompt === 'string' ? { prompt } : { messages: prompt }),
|
|
204
|
-
...(typeof temperature === 'number' ? { temperature } : {}),
|
|
205
|
-
...(shouldSendMaxOutputTokens() ? { maxOutputTokens } : {}),
|
|
206
|
-
abortSignal: controller.signal,
|
|
207
|
-
});
|
|
208
|
-
assertNonEmptyText(result.text, parsed.canonical);
|
|
409
|
+
const text = extractText(result);
|
|
410
|
+
if (!text)
|
|
411
|
+
throw new Error(`LLM returned an empty summary (model ${parsed.canonical}).`);
|
|
209
412
|
return {
|
|
210
|
-
text
|
|
413
|
+
text,
|
|
211
414
|
canonicalModelId: parsed.canonical,
|
|
212
415
|
provider: parsed.provider,
|
|
213
416
|
usage: normalizeTokenUsage(result.usage),
|
|
@@ -257,8 +460,10 @@ function computeRetryDelayMs(attempt) {
|
|
|
257
460
|
function sleep(ms) {
|
|
258
461
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
259
462
|
}
|
|
260
|
-
export async function streamTextWithModelId({ modelId, apiKeys, system, prompt, temperature, maxOutputTokens, timeoutMs, fetchImpl, forceOpenRouter, openaiBaseUrlOverride, forceChatCompletions, }) {
|
|
463
|
+
export async function streamTextWithModelId({ modelId, apiKeys, system, prompt, temperature, maxOutputTokens, timeoutMs, fetchImpl: _fetchImpl, forceOpenRouter, openaiBaseUrlOverride, anthropicBaseUrlOverride, googleBaseUrlOverride, xaiBaseUrlOverride, forceChatCompletions, }) {
|
|
464
|
+
void _fetchImpl;
|
|
261
465
|
const parsed = parseGatewayStyleModelId(modelId);
|
|
466
|
+
const context = promptToContext({ system, prompt });
|
|
262
467
|
const controller = new AbortController();
|
|
263
468
|
let timeoutId = null;
|
|
264
469
|
const startedAtMs = Date.now();
|
|
@@ -330,36 +535,42 @@ export async function streamTextWithModelId({ modelId, apiKeys, system, prompt,
|
|
|
330
535
|
},
|
|
331
536
|
});
|
|
332
537
|
try {
|
|
333
|
-
const { streamText } = await import('ai');
|
|
334
|
-
const onError = ({ error }) => {
|
|
335
|
-
if (parsed.provider === 'anthropic') {
|
|
336
|
-
lastError = normalizeAnthropicModelAccessError(error, parsed.model) ?? error;
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
339
|
-
lastError = error;
|
|
340
|
-
};
|
|
341
|
-
const shouldSendMaxOutputTokens = () => typeof maxOutputTokens === 'number';
|
|
342
538
|
if (parsed.provider === 'xai') {
|
|
343
539
|
const apiKey = apiKeys.xaiApiKey;
|
|
344
540
|
if (!apiKey)
|
|
345
541
|
throw new Error('Missing XAI_API_KEY for xai/... model');
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
542
|
+
const model = resolveModelForCall({
|
|
543
|
+
modelId: parsed.model,
|
|
544
|
+
parsedProvider: parsed.provider,
|
|
545
|
+
openaiConfig: null,
|
|
546
|
+
context,
|
|
547
|
+
xaiBaseUrlOverride,
|
|
548
|
+
});
|
|
549
|
+
const stream = streamSimple(model, context, {
|
|
352
550
|
...(typeof temperature === 'number' ? { temperature } : {}),
|
|
353
|
-
...(
|
|
354
|
-
|
|
355
|
-
|
|
551
|
+
...(typeof maxOutputTokens === 'number' ? { maxTokens: maxOutputTokens } : {}),
|
|
552
|
+
apiKey,
|
|
553
|
+
signal: controller.signal,
|
|
356
554
|
});
|
|
555
|
+
const textStream = {
|
|
556
|
+
async *[Symbol.asyncIterator]() {
|
|
557
|
+
for await (const event of stream) {
|
|
558
|
+
if (event.type === 'text_delta')
|
|
559
|
+
yield event.delta;
|
|
560
|
+
if (event.type === 'error') {
|
|
561
|
+
lastError = event.error;
|
|
562
|
+
break;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
},
|
|
566
|
+
};
|
|
357
567
|
return {
|
|
358
|
-
textStream: wrapTextStream(
|
|
568
|
+
textStream: wrapTextStream(textStream),
|
|
359
569
|
canonicalModelId: parsed.canonical,
|
|
360
570
|
provider: parsed.provider,
|
|
361
|
-
usage:
|
|
362
|
-
.
|
|
571
|
+
usage: stream
|
|
572
|
+
.result()
|
|
573
|
+
.then((msg) => normalizeTokenUsage(msg.usage))
|
|
363
574
|
.catch(() => null),
|
|
364
575
|
lastError: () => lastError,
|
|
365
576
|
};
|
|
@@ -368,23 +579,38 @@ export async function streamTextWithModelId({ modelId, apiKeys, system, prompt,
|
|
|
368
579
|
const apiKey = apiKeys.googleApiKey;
|
|
369
580
|
if (!apiKey)
|
|
370
581
|
throw new Error('Missing GEMINI_API_KEY (or GOOGLE_GENERATIVE_AI_API_KEY / GOOGLE_API_KEY) for google/... model');
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
582
|
+
const model = resolveModelForCall({
|
|
583
|
+
modelId: parsed.model,
|
|
584
|
+
parsedProvider: parsed.provider,
|
|
585
|
+
openaiConfig: null,
|
|
586
|
+
context,
|
|
587
|
+
googleBaseUrlOverride,
|
|
588
|
+
});
|
|
589
|
+
const stream = streamSimple(model, context, {
|
|
377
590
|
...(typeof temperature === 'number' ? { temperature } : {}),
|
|
378
|
-
...(
|
|
379
|
-
|
|
380
|
-
|
|
591
|
+
...(typeof maxOutputTokens === 'number' ? { maxTokens: maxOutputTokens } : {}),
|
|
592
|
+
apiKey,
|
|
593
|
+
signal: controller.signal,
|
|
381
594
|
});
|
|
595
|
+
const textStream = {
|
|
596
|
+
async *[Symbol.asyncIterator]() {
|
|
597
|
+
for await (const event of stream) {
|
|
598
|
+
if (event.type === 'text_delta')
|
|
599
|
+
yield event.delta;
|
|
600
|
+
if (event.type === 'error') {
|
|
601
|
+
lastError = event.error;
|
|
602
|
+
break;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
},
|
|
606
|
+
};
|
|
382
607
|
return {
|
|
383
|
-
textStream: wrapTextStream(
|
|
608
|
+
textStream: wrapTextStream(textStream),
|
|
384
609
|
canonicalModelId: parsed.canonical,
|
|
385
610
|
provider: parsed.provider,
|
|
386
|
-
usage:
|
|
387
|
-
.
|
|
611
|
+
usage: stream
|
|
612
|
+
.result()
|
|
613
|
+
.then((msg) => normalizeTokenUsage(msg.usage))
|
|
388
614
|
.catch(() => null),
|
|
389
615
|
lastError: () => lastError,
|
|
390
616
|
};
|
|
@@ -393,59 +619,124 @@ export async function streamTextWithModelId({ modelId, apiKeys, system, prompt,
|
|
|
393
619
|
const apiKey = apiKeys.anthropicApiKey;
|
|
394
620
|
if (!apiKey)
|
|
395
621
|
throw new Error('Missing ANTHROPIC_API_KEY for anthropic/... model');
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
622
|
+
const model = resolveModelForCall({
|
|
623
|
+
modelId: parsed.model,
|
|
624
|
+
parsedProvider: parsed.provider,
|
|
625
|
+
openaiConfig: null,
|
|
626
|
+
context,
|
|
627
|
+
anthropicBaseUrlOverride,
|
|
628
|
+
});
|
|
629
|
+
const stream = streamSimple(model, context, {
|
|
402
630
|
...(typeof temperature === 'number' ? { temperature } : {}),
|
|
403
|
-
...(
|
|
404
|
-
|
|
405
|
-
|
|
631
|
+
...(typeof maxOutputTokens === 'number' ? { maxTokens: maxOutputTokens } : {}),
|
|
632
|
+
apiKey,
|
|
633
|
+
signal: controller.signal,
|
|
406
634
|
});
|
|
635
|
+
const textStream = {
|
|
636
|
+
async *[Symbol.asyncIterator]() {
|
|
637
|
+
for await (const event of stream) {
|
|
638
|
+
if (event.type === 'text_delta')
|
|
639
|
+
yield event.delta;
|
|
640
|
+
if (event.type === 'error') {
|
|
641
|
+
lastError =
|
|
642
|
+
normalizeAnthropicModelAccessError(event.error, parsed.model) ?? event.error;
|
|
643
|
+
break;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
},
|
|
647
|
+
};
|
|
407
648
|
return {
|
|
408
|
-
textStream: wrapTextStream(
|
|
649
|
+
textStream: wrapTextStream(textStream),
|
|
409
650
|
canonicalModelId: parsed.canonical,
|
|
410
651
|
provider: parsed.provider,
|
|
411
|
-
usage:
|
|
412
|
-
.
|
|
652
|
+
usage: stream
|
|
653
|
+
.result()
|
|
654
|
+
.then((msg) => normalizeTokenUsage(msg.usage))
|
|
655
|
+
.catch(() => null),
|
|
656
|
+
lastError: () => lastError,
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
if (parsed.provider === 'zai') {
|
|
660
|
+
const apiKey = apiKeys.openaiApiKey;
|
|
661
|
+
if (!apiKey)
|
|
662
|
+
throw new Error('Missing Z_AI_API_KEY for zai/... model');
|
|
663
|
+
const model = resolveModelForCall({
|
|
664
|
+
modelId: parsed.model,
|
|
665
|
+
parsedProvider: parsed.provider,
|
|
666
|
+
openaiConfig: null,
|
|
667
|
+
context,
|
|
668
|
+
openaiBaseUrlOverride,
|
|
669
|
+
});
|
|
670
|
+
const stream = streamSimple(model, context, {
|
|
671
|
+
...(typeof temperature === 'number' ? { temperature } : {}),
|
|
672
|
+
...(typeof maxOutputTokens === 'number' ? { maxTokens: maxOutputTokens } : {}),
|
|
673
|
+
apiKey,
|
|
674
|
+
signal: controller.signal,
|
|
675
|
+
});
|
|
676
|
+
const textStream = {
|
|
677
|
+
async *[Symbol.asyncIterator]() {
|
|
678
|
+
for await (const event of stream) {
|
|
679
|
+
if (event.type === 'text_delta')
|
|
680
|
+
yield event.delta;
|
|
681
|
+
if (event.type === 'error') {
|
|
682
|
+
lastError = event.error;
|
|
683
|
+
break;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
},
|
|
687
|
+
};
|
|
688
|
+
return {
|
|
689
|
+
textStream: wrapTextStream(textStream),
|
|
690
|
+
canonicalModelId: parsed.canonical,
|
|
691
|
+
provider: parsed.provider,
|
|
692
|
+
usage: stream
|
|
693
|
+
.result()
|
|
694
|
+
.then((msg) => normalizeTokenUsage(msg.usage))
|
|
413
695
|
.catch(() => null),
|
|
414
696
|
lastError: () => lastError,
|
|
415
697
|
};
|
|
416
698
|
}
|
|
417
|
-
const { createOpenAI } = await import('@ai-sdk/openai');
|
|
418
699
|
const openaiConfig = resolveOpenAiClientConfig({
|
|
419
700
|
apiKeys,
|
|
420
|
-
fetchImpl,
|
|
421
701
|
forceOpenRouter,
|
|
422
702
|
openaiBaseUrlOverride,
|
|
423
703
|
forceChatCompletions,
|
|
424
704
|
});
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
705
|
+
const model = resolveModelForCall({
|
|
706
|
+
modelId: parsed.model,
|
|
707
|
+
parsedProvider: parsed.provider,
|
|
708
|
+
openaiConfig,
|
|
709
|
+
context,
|
|
710
|
+
openaiBaseUrlOverride,
|
|
711
|
+
anthropicBaseUrlOverride,
|
|
712
|
+
googleBaseUrlOverride,
|
|
713
|
+
xaiBaseUrlOverride,
|
|
429
714
|
});
|
|
430
|
-
|
|
431
|
-
const useChatCompletions = openaiConfig.useChatCompletions;
|
|
432
|
-
const responsesModelId = parsed.model;
|
|
433
|
-
const chatModelId = parsed.model;
|
|
434
|
-
const result = streamText({
|
|
435
|
-
model: useChatCompletions ? openai.chat(chatModelId) : openai(responsesModelId),
|
|
436
|
-
system,
|
|
437
|
-
...(typeof prompt === 'string' ? { prompt } : { messages: prompt }),
|
|
715
|
+
const stream = streamSimple(model, context, {
|
|
438
716
|
...(typeof temperature === 'number' ? { temperature } : {}),
|
|
439
|
-
...(
|
|
440
|
-
|
|
441
|
-
|
|
717
|
+
...(typeof maxOutputTokens === 'number' ? { maxTokens: maxOutputTokens } : {}),
|
|
718
|
+
apiKey: openaiConfig.apiKey,
|
|
719
|
+
signal: controller.signal,
|
|
442
720
|
});
|
|
721
|
+
const textStream = {
|
|
722
|
+
async *[Symbol.asyncIterator]() {
|
|
723
|
+
for await (const event of stream) {
|
|
724
|
+
if (event.type === 'text_delta')
|
|
725
|
+
yield event.delta;
|
|
726
|
+
if (event.type === 'error') {
|
|
727
|
+
lastError = event.error;
|
|
728
|
+
break;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
},
|
|
732
|
+
};
|
|
443
733
|
return {
|
|
444
|
-
textStream: wrapTextStream(
|
|
734
|
+
textStream: wrapTextStream(textStream),
|
|
445
735
|
canonicalModelId: parsed.canonical,
|
|
446
736
|
provider: parsed.provider,
|
|
447
|
-
usage:
|
|
448
|
-
.
|
|
737
|
+
usage: stream
|
|
738
|
+
.result()
|
|
739
|
+
.then((msg) => normalizeTokenUsage(msg.usage))
|
|
449
740
|
.catch(() => null),
|
|
450
741
|
lastError: () => lastError,
|
|
451
742
|
};
|