@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.
Files changed (151) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +53 -2
  3. package/dist/cli.js +3 -0
  4. package/dist/esm/cache.js +353 -0
  5. package/dist/esm/cache.js.map +1 -0
  6. package/dist/esm/config.js +78 -1
  7. package/dist/esm/config.js.map +1 -1
  8. package/dist/esm/content/asset.js +11 -17
  9. package/dist/esm/content/asset.js.map +1 -1
  10. package/dist/esm/daemon/auto-mode.js +8 -0
  11. package/dist/esm/daemon/auto-mode.js.map +1 -0
  12. package/dist/esm/daemon/cli.js +284 -0
  13. package/dist/esm/daemon/cli.js.map +1 -0
  14. package/dist/esm/daemon/config.js +82 -0
  15. package/dist/esm/daemon/config.js.map +1 -0
  16. package/dist/esm/daemon/constants.js +8 -0
  17. package/dist/esm/daemon/constants.js.map +1 -0
  18. package/dist/esm/daemon/env-merge.js +4 -0
  19. package/dist/esm/daemon/env-merge.js.map +1 -0
  20. package/dist/esm/daemon/env-snapshot.js +43 -0
  21. package/dist/esm/daemon/env-snapshot.js.map +1 -0
  22. package/dist/esm/daemon/flow-context.js +265 -0
  23. package/dist/esm/daemon/flow-context.js.map +1 -0
  24. package/dist/esm/daemon/launchd.js +149 -0
  25. package/dist/esm/daemon/launchd.js.map +1 -0
  26. package/dist/esm/daemon/meta.js +35 -0
  27. package/dist/esm/daemon/meta.js.map +1 -0
  28. package/dist/esm/daemon/models.js +175 -0
  29. package/dist/esm/daemon/models.js.map +1 -0
  30. package/dist/esm/daemon/request-settings.js +91 -0
  31. package/dist/esm/daemon/request-settings.js.map +1 -0
  32. package/dist/esm/daemon/schtasks.js +108 -0
  33. package/dist/esm/daemon/schtasks.js.map +1 -0
  34. package/dist/esm/daemon/server.js +399 -0
  35. package/dist/esm/daemon/server.js.map +1 -0
  36. package/dist/esm/daemon/summarize-progress.js +57 -0
  37. package/dist/esm/daemon/summarize-progress.js.map +1 -0
  38. package/dist/esm/daemon/summarize.js +263 -0
  39. package/dist/esm/daemon/summarize.js.map +1 -0
  40. package/dist/esm/daemon/systemd.js +117 -0
  41. package/dist/esm/daemon/systemd.js.map +1 -0
  42. package/dist/esm/flags.js +3 -1
  43. package/dist/esm/flags.js.map +1 -1
  44. package/dist/esm/llm/generate-text.js +445 -154
  45. package/dist/esm/llm/generate-text.js.map +1 -1
  46. package/dist/esm/llm/html-to-markdown.js +4 -1
  47. package/dist/esm/llm/html-to-markdown.js.map +1 -1
  48. package/dist/esm/llm/prompt.js +14 -0
  49. package/dist/esm/llm/prompt.js.map +1 -0
  50. package/dist/esm/llm/transcript-to-markdown.js +57 -0
  51. package/dist/esm/llm/transcript-to-markdown.js.map +1 -0
  52. package/dist/esm/model-spec.js +2 -2
  53. package/dist/esm/model-spec.js.map +1 -1
  54. package/dist/esm/run/attachments.js +10 -42
  55. package/dist/esm/run/attachments.js.map +1 -1
  56. package/dist/esm/run/cache-state.js +48 -0
  57. package/dist/esm/run/cache-state.js.map +1 -0
  58. package/dist/esm/run/cli-preflight.js +15 -1
  59. package/dist/esm/run/cli-preflight.js.map +1 -1
  60. package/dist/esm/run/cookies/twitter.js +224 -0
  61. package/dist/esm/run/cookies/twitter.js.map +1 -0
  62. package/dist/esm/run/fetch-with-timeout.js +1 -1
  63. package/dist/esm/run/fetch-with-timeout.js.map +1 -1
  64. package/dist/esm/run/finish-line.js +46 -17
  65. package/dist/esm/run/finish-line.js.map +1 -1
  66. package/dist/esm/run/flows/asset/input.js +2 -4
  67. package/dist/esm/run/flows/asset/input.js.map +1 -1
  68. package/dist/esm/run/flows/asset/preprocess.js +52 -72
  69. package/dist/esm/run/flows/asset/preprocess.js.map +1 -1
  70. package/dist/esm/run/flows/asset/summary.js +127 -47
  71. package/dist/esm/run/flows/asset/summary.js.map +1 -1
  72. package/dist/esm/run/flows/url/extract.js +6 -1
  73. package/dist/esm/run/flows/url/extract.js.map +1 -1
  74. package/dist/esm/run/flows/url/flow.js +166 -85
  75. package/dist/esm/run/flows/url/flow.js.map +1 -1
  76. package/dist/esm/run/flows/url/markdown.js +88 -46
  77. package/dist/esm/run/flows/url/markdown.js.map +1 -1
  78. package/dist/esm/run/flows/url/summary.js +263 -185
  79. package/dist/esm/run/flows/url/summary.js.map +1 -1
  80. package/dist/esm/run/help.js +33 -2
  81. package/dist/esm/run/help.js.map +1 -1
  82. package/dist/esm/run/run-env.js +36 -2
  83. package/dist/esm/run/run-env.js.map +1 -1
  84. package/dist/esm/run/runner.js +362 -227
  85. package/dist/esm/run/runner.js.map +1 -1
  86. package/dist/esm/run/summary-engine.js +21 -6
  87. package/dist/esm/run/summary-engine.js.map +1 -1
  88. package/dist/esm/run/summary-llm.js +4 -1
  89. package/dist/esm/run/summary-llm.js.map +1 -1
  90. package/dist/esm/tty/format.js +9 -0
  91. package/dist/esm/tty/format.js.map +1 -1
  92. package/dist/esm/version.js +1 -1
  93. package/dist/types/cache.d.ts +70 -0
  94. package/dist/types/config.d.ts +46 -0
  95. package/dist/types/content/asset.d.ts +4 -3
  96. package/dist/types/daemon/auto-mode.d.ts +8 -0
  97. package/dist/types/daemon/cli.d.ts +9 -0
  98. package/dist/types/daemon/config.d.ts +19 -0
  99. package/dist/types/daemon/constants.d.ts +7 -0
  100. package/dist/types/daemon/env-merge.d.ts +5 -0
  101. package/dist/types/daemon/env-snapshot.d.ts +4 -0
  102. package/dist/types/daemon/flow-context.d.ts +28 -0
  103. package/dist/types/daemon/launchd.d.ts +29 -0
  104. package/dist/types/daemon/meta.d.ts +12 -0
  105. package/dist/types/daemon/models.d.ts +27 -0
  106. package/dist/types/daemon/request-settings.d.ts +27 -0
  107. package/dist/types/daemon/schtasks.d.ts +16 -0
  108. package/dist/types/daemon/server.d.ts +12 -0
  109. package/dist/types/daemon/summarize-progress.d.ts +2 -0
  110. package/dist/types/daemon/summarize.d.ts +59 -0
  111. package/dist/types/daemon/systemd.d.ts +16 -0
  112. package/dist/types/flags.d.ts +1 -1
  113. package/dist/types/llm/generate-text.d.ts +11 -5
  114. package/dist/types/llm/html-to-markdown.d.ts +4 -1
  115. package/dist/types/llm/prompt.d.ts +9 -0
  116. package/dist/types/llm/transcript-to-markdown.d.ts +34 -0
  117. package/dist/types/run/attachments.d.ts +4 -10
  118. package/dist/types/run/cache-state.d.ts +12 -0
  119. package/dist/types/run/cli-preflight.d.ts +1 -0
  120. package/dist/types/run/cookies/twitter.d.ts +17 -0
  121. package/dist/types/run/finish-line.d.ts +31 -1
  122. package/dist/types/run/flows/asset/preprocess.d.ts +5 -2
  123. package/dist/types/run/flows/asset/summary.d.ts +11 -0
  124. package/dist/types/run/flows/url/markdown.d.ts +3 -0
  125. package/dist/types/run/flows/url/summary.d.ts +6 -3
  126. package/dist/types/run/flows/url/types.d.ts +52 -18
  127. package/dist/types/run/help.d.ts +1 -0
  128. package/dist/types/run/run-env.d.ts +6 -0
  129. package/dist/types/run/summary-engine.d.ts +8 -2
  130. package/dist/types/run/summary-llm.d.ts +6 -3
  131. package/dist/types/tty/format.d.ts +1 -0
  132. package/dist/types/version.d.ts +1 -1
  133. package/docs/README.md +5 -0
  134. package/docs/cache.md +72 -0
  135. package/docs/chrome-extension.md +180 -0
  136. package/docs/cli.md +6 -0
  137. package/docs/config.md +65 -1
  138. package/docs/extract-only.md +6 -0
  139. package/docs/firecrawl.md +6 -0
  140. package/docs/language.md +6 -0
  141. package/docs/llm.md +20 -0
  142. package/docs/manual-tests.md +6 -0
  143. package/docs/model-auto.md +6 -0
  144. package/docs/openai.md +6 -0
  145. package/docs/site/index.html +11 -1
  146. package/docs/smoketest.md +6 -0
  147. package/docs/website.md +6 -0
  148. package/docs/youtube.md +9 -2
  149. package/package.json +7 -10
  150. package/dist/cli.cjs +0 -80566
  151. 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.promptTokens === 'number' && Number.isFinite(usage.promptTokens)
52
- ? usage.promptTokens
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 resolveOpenAiClientConfig({ apiKeys, fetchImpl, forceOpenRouter, openaiBaseUrlOverride, forceChatCompletions, }) {
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 useChatCompletions = Boolean(forceChatCompletions) || isOpenRouter;
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
- export async function generateTextWithModelId({ modelId, apiKeys, system, prompt, temperature, maxOutputTokens, timeoutMs, fetchImpl, forceOpenRouter, openaiBaseUrlOverride, forceChatCompletions, retries = 0, onRetry, }) {
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 { createXai } = await import('@ai-sdk/xai');
122
- const xai = createXai({ apiKey, fetch: fetchImpl });
123
- const result = await generateText({
124
- model: xai(parsed.model),
125
- system,
126
- ...(typeof prompt === 'string' ? { prompt } : { messages: prompt }),
127
- ...(typeof temperature === 'number' ? { temperature } : {}),
128
- ...(shouldSendMaxOutputTokens() ? { maxOutputTokens } : {}),
129
- abortSignal: controller.signal,
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
- assertNonEmptyText(result.text, parsed.canonical);
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: result.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 { createGoogleGenerativeAI } = await import('@ai-sdk/google');
144
- const google = createGoogleGenerativeAI({ apiKey, fetch: fetchImpl });
145
- const result = await generateText({
146
- model: google(parsed.model),
147
- system,
148
- ...(typeof prompt === 'string' ? { prompt } : { messages: prompt }),
149
- ...(typeof temperature === 'number' ? { temperature } : {}),
150
- ...(shouldSendMaxOutputTokens() ? { maxOutputTokens } : {}),
151
- abortSignal: controller.signal,
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
- assertNonEmptyText(result.text, parsed.canonical);
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: result.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 { createAnthropic } = await import('@ai-sdk/anthropic');
166
- const anthropic = createAnthropic({ apiKey, fetch: fetchImpl });
167
- const result = await generateText({
168
- model: anthropic(parsed.model),
169
- system,
170
- ...(typeof prompt === 'string' ? { prompt } : { messages: prompt }),
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
- assertNonEmptyText(result.text, parsed.canonical);
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: result.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 { createOpenAI } = await import('@ai-sdk/openai');
184
- const openaiConfig = resolveOpenAiClientConfig({
185
- apiKeys,
186
- fetchImpl,
187
- forceOpenRouter,
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
- forceChatCompletions,
399
+ anthropicBaseUrlOverride,
400
+ googleBaseUrlOverride,
401
+ xaiBaseUrlOverride,
190
402
  });
191
- const openai = createOpenAI({
192
- apiKey: openaiConfig.apiKey,
193
- ...(openaiConfig.baseURL ? { baseURL: openaiConfig.baseURL } : {}),
194
- fetch: openaiConfig.fetch,
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
- // OpenRouter requires chat completions endpoint
197
- const useChatCompletions = openaiConfig.useChatCompletions;
198
- const responsesModelId = parsed.model;
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: result.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 { createXai } = await import('@ai-sdk/xai');
347
- const xai = createXai({ apiKey, fetch: fetchImpl });
348
- const result = streamText({
349
- model: xai(parsed.model),
350
- system,
351
- ...(typeof prompt === 'string' ? { prompt } : { messages: prompt }),
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
- ...(shouldSendMaxOutputTokens() ? { maxOutputTokens } : {}),
354
- abortSignal: controller.signal,
355
- onError,
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(result.textStream),
568
+ textStream: wrapTextStream(textStream),
359
569
  canonicalModelId: parsed.canonical,
360
570
  provider: parsed.provider,
361
- usage: Promise.resolve(result.totalUsage)
362
- .then((raw) => normalizeTokenUsage(raw))
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 { createGoogleGenerativeAI } = await import('@ai-sdk/google');
372
- const google = createGoogleGenerativeAI({ apiKey, fetch: fetchImpl });
373
- const result = streamText({
374
- model: google(parsed.model),
375
- system,
376
- ...(typeof prompt === 'string' ? { prompt } : { messages: prompt }),
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
- ...(shouldSendMaxOutputTokens() ? { maxOutputTokens } : {}),
379
- abortSignal: controller.signal,
380
- onError,
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(result.textStream),
608
+ textStream: wrapTextStream(textStream),
384
609
  canonicalModelId: parsed.canonical,
385
610
  provider: parsed.provider,
386
- usage: Promise.resolve(result.totalUsage)
387
- .then((raw) => normalizeTokenUsage(raw))
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 { createAnthropic } = await import('@ai-sdk/anthropic');
397
- const anthropic = createAnthropic({ apiKey, fetch: fetchImpl });
398
- const result = streamText({
399
- model: anthropic(parsed.model),
400
- system,
401
- ...(typeof prompt === 'string' ? { prompt } : { messages: prompt }),
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
- ...(shouldSendMaxOutputTokens() ? { maxOutputTokens } : {}),
404
- abortSignal: controller.signal,
405
- onError,
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(result.textStream),
649
+ textStream: wrapTextStream(textStream),
409
650
  canonicalModelId: parsed.canonical,
410
651
  provider: parsed.provider,
411
- usage: Promise.resolve(result.totalUsage)
412
- .then((raw) => normalizeTokenUsage(raw))
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 openai = createOpenAI({
426
- apiKey: openaiConfig.apiKey,
427
- ...(openaiConfig.baseURL ? { baseURL: openaiConfig.baseURL } : {}),
428
- fetch: openaiConfig.fetch,
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
- // OpenRouter requires chat completions endpoint
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
- ...(shouldSendMaxOutputTokens() ? { maxOutputTokens } : {}),
440
- abortSignal: controller.signal,
441
- onError,
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(result.textStream),
734
+ textStream: wrapTextStream(textStream),
445
735
  canonicalModelId: parsed.canonical,
446
736
  provider: parsed.provider,
447
- usage: Promise.resolve(result.totalUsage)
448
- .then((raw) => normalizeTokenUsage(raw))
737
+ usage: stream
738
+ .result()
739
+ .then((msg) => normalizeTokenUsage(msg.usage))
449
740
  .catch(() => null),
450
741
  lastError: () => lastError,
451
742
  };