@combycode/llm-sdk 1.6.1 → 1.7.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/CHANGELOG.md CHANGED
@@ -6,6 +6,94 @@ All notable changes to `@combycode/llm-sdk` are documented here. The format foll
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.7.0] - 2026-07-16
10
+
11
+ ### Added
12
+ - **Video extend + edit (xAI grok-imagine-video).** `VideoGenRequest` gains `sourceVideo?: DataSource`
13
+ and `params.videoMode?: 'extend' | 'edit'`. When a source video is present the xAI adapter routes to
14
+ the right endpoint instead of plain generation: `extend` (default) → `POST /v1/videos/extensions`
15
+ (continues from the last frame; takes `duration`, ignores aspect/resolution), `edit` →
16
+ `POST /v1/videos/edits` (prompt + video only). The clip is passed as a public URL, a Files-API id, or
17
+ an inline base64 data-URL. `MediaCapabilities` gains `videoExtension`; `MediaOutput.generateVideo`
18
+ throws if a `sourceVideo` is sent to a provider that doesn't support it (rather than silently
19
+ generating). `generateVideo(req)` is unchanged — the new fields flow through. Verified live end-to-end
20
+ on `grok-imagine-video`: generate → extend → edit all return video bytes. (Note: extend/edit require
21
+ `grok-imagine-video`, not `grok-imagine-video-1.5`, which is generation-only.)
22
+ - **`onMediaProgress` hook.** Long-running async video ops (generate/extend/edit) now emit an
23
+ `onMediaProgress` event once per poll (`{ type, provider, operationId, progress, model }`), so a UI can
24
+ render a progress bar. Progress values confirmed live (0→100).
25
+ - **`RawMediaResult.sourceUrl` + `MediaMeta.sourceUrl`.** Async video results now carry the provider's
26
+ hosted URL, so callers can render or re-submit the asset without holding the bytes.
27
+ - **Unified reasoning visibility.** `ThinkingConfig` gains `visibility: 'full' (default) | 'summary' |
28
+ 'hidden'`, mapping to Anthropic `enabled.display`, OpenAI Responses `summary`, and Google
29
+ `includeThoughts` — one knob for "how much reasoning comes back" across providers (best-effort; a
30
+ provider without a middle state degrades `summary` to full). Default `full` = prior behaviour.
31
+ - **OpenAI reasoning execution mode.** `providerOptions.reasoningMode: 'standard' | 'pro'` maps to
32
+ `reasoning.mode` on the OpenAI **Responses** path (chat-completions rejects it). Kept in
33
+ `providerOptions` rather than a first-class knob since only one provider/API honours it.
34
+ - **Google `translationConfig` passthrough.** `providerOptions.translationConfig` forwards to
35
+ `generationConfig.translationConfig` on generateContent (Gemini Developer API; live-verified 200).
36
+ - **OpenAI native moderation blocking.** `providerOptions.moderationPolicy`
37
+ (`{ input?: { mode: 'score'|'block' }, output?: {…} }`) forwards to OpenAI's `moderation.policy` on
38
+ Responses + chat for server-side blocking. Unified moderation stays report-only by design (blocking is
39
+ `moderationGuardrail` at the agent layer); this is an OpenAI-specific opt-in, so it lives in
40
+ `providerOptions`. Live-verified the field is accepted.
41
+ - **OpenAI explicit prompt caching.** `providerOptions.promptCacheOptions`
42
+ (`{ mode: 'implicit'|'explicit', ttl: '30m' }`) forwards to `prompt_cache_options` (gpt-5.6+),
43
+ true-OpenAI only (xai/openrouter inherit the builder and don't emit it). Note: OpenAI caches
44
+ **implicitly by default**, so the unified `cache` config already works there with no config — this
45
+ passthrough is for manual control. (`prompt_cache_retention` is deprecated upstream in favour of
46
+ `prompt_cache_options.ttl`.)
47
+ - **OpenAI programmatic tool calling (Responses).** `BuiltinTool` gains `programmatic_tool_calling`, and
48
+ `FunctionTool` gains `allowedCallers?: ('direct'|'programmatic')[]` + `outputSchema?` — emitted on the
49
+ OpenAI Responses path only. Live-verified on gpt-5.6 (tool calls succeed; older models reject the
50
+ builtin, which is model-gated). (Surfacing the `program`/`program_output` output items is deferred; the
51
+ parser already tolerates them without error.)
52
+
53
+ ### Fixed
54
+ - **OpenAI prompt-cache write tokens were dropped.** Both usage parsers hardcoded `cacheWriteTokens: 0`;
55
+ they now read `input_tokens_details.cache_write_tokens` (Responses) / `prompt_tokens_details.
56
+ cache_write_tokens` (chat), so cost accounting no longer under-reports explicit prompt caching.
57
+ - **Google reasoning was live-broken (two paths).** generateContent sent `thinkingLevel`, which the
58
+ Gemini Developer API 400s on **2.5** models (it is 3.x-only) — now routed per series: 2.5 →
59
+ `thinkingBudget` (token count), 3.x → `thinkingLevel`. The Interactions path wrapped `thinking_config`
60
+ (rejected outright) and used uppercase values — it takes `thinking_level` **flat** on `generation_config`
61
+ and **lowercase** (`minimal/low/medium/high`). Both live-verified across gemini-2.5 + 3.5.
62
+ - **Google Interactions rejected sampling penalties.** We emitted `presence_penalty`/`frequency_penalty`
63
+ on the Interactions path, which the API 400s ("Unknown parameter") — upstream removed them from its
64
+ Interactions config. No longer emitted there (still valid on generateContent).
65
+ - **Streamed tool-call id collision on OpenAI-compatible backends.** The chat-completions stream parser
66
+ keyed tool-call fragments by `id ?? ''`, so parallel calls from backends that omit ids (LiteLLM/Bedrock,
67
+ some OpenRouter routes) merged into one. Fragments are now correlated by `index`, with a stable
68
+ `call_<uuid>` synthesized once per index when the backend omits ids.
69
+ - **`content_filter` finish reason was flattened to `stop`/`length`.** The chat-completions stream reason
70
+ map lacked `content_filter`, `AgentLoop` derived every normal-completion finish as `stop`, and the
71
+ Responses parser mapped `status: 'incomplete'` to `length` regardless of `incomplete_details.reason` —
72
+ all discarding the provider's actual reason. Now the stream map, the loop, and the Responses parser
73
+ (reading `incomplete_details.reason`) all surface `content_filter` (and `length`) to consumers — so a
74
+ moderation/safety block is distinguishable from a token cap. (Our loop already terminates on non-tool
75
+ finishes, so it never retry-looped on an empty filtered turn.)
76
+ - **Browser: xAI video result was unusable (CORS).** The generated clip lives on a cross-origin bucket
77
+ (`vidgen.x.ai`) that sends no `Access-Control-Allow-Origin`, so `downloadVideo`'s programmatic
78
+ byte-fetch was blocked in the browser and video generation failed outright. In the browser the adapter
79
+ now returns the hosted URL (via `sourceUrl`) with empty bytes instead of fetching — `<video src>` plays
80
+ it cross-origin without CORS, and it can be re-submitted as a `sourceVideo`. Node/Bun still download the
81
+ bytes. (Node-only tests couldn't surface this; CORS isn't enforced off-browser.)
82
+ - **Google `editImage` aspect ratio / size (same bug, second code path).** The 1.6.1 fix moved
83
+ `aspectRatio` / `imageSize` to `generationConfig.imageConfig` only in `generateImage`; the sibling
84
+ `editImage` method still wrote `generationConfig.responseFormat.image` and so 400'd on any edit that
85
+ passed an aspect ratio or size. Now both image paths use `imageConfig`. Verified live end-to-end:
86
+ generate → edit round-trip both return an image at `16:9` / `2K`. Locked with a unit regression on the
87
+ `editImage` request body.
88
+ - **xAI video generation polled forever after the job finished.** `getVideoStatus` only treated
89
+ `status: "completed"`/`"ready"` (or a `download_url`) as done, but xAI reports terminal success as
90
+ `status: "done"` with the URL under `video.url` — so a finished job kept polling until the wait cap and
91
+ never returned. `downloadVideo` likewise read `download_url`/`url` and missed `video.url` (and duration
92
+ under `video.duration`). Both now read the real `video.*` shape (flat fallbacks kept), and terminal
93
+ `status: "done"`/`"expired"` are handled. Progress is now carried on the processing status. Verified
94
+ live end-to-end (grok-imagine-video-1.5 image-to-video: progress 0→75→100 → downloaded 2.5 MB); locked
95
+ with a unit test replaying the real server payload.
96
+
9
97
  ## [1.6.1] - 2026-07-13
10
98
 
11
99
  ### Fixed
@@ -467,6 +467,16 @@ export interface MediaErrorContext {
467
467
  /** For async ops (video). */
468
468
  operationId?: string;
469
469
  }
470
+ /** Progress ping for a long-running async media op (video generation/extend/
471
+ * edit) — emitted once per poll while the job runs, so a UI can show a bar. */
472
+ export interface MediaProgressContext {
473
+ type: 'video';
474
+ provider: string;
475
+ operationId: string;
476
+ /** 0–100 when the provider reports it; omitted otherwise. */
477
+ progress?: number;
478
+ model?: string;
479
+ }
470
480
  export interface ServerRequestContext {
471
481
  serverId: string;
472
482
  requestId: string;
@@ -581,6 +591,7 @@ export interface HookMap {
581
591
  onContextMeasure: ContextMeasureContext;
582
592
  onMediaGenerated: MediaGeneratedContext;
583
593
  onMediaError: MediaErrorContext;
594
+ onMediaProgress: MediaProgressContext;
584
595
  onInternalToolCallStart: InternalToolCallStartContext;
585
596
  onInternalToolCallComplete: InternalToolCallCompleteContext;
586
597
  onInternalToolCallError: InternalToolCallErrorContext;
@@ -23491,7 +23491,8 @@ var catalog_default5 = {
23491
23491
  video: false,
23492
23492
  imageGeneration: false,
23493
23493
  audioGeneration: false,
23494
- videoGeneration: true
23494
+ videoGeneration: true,
23495
+ videoExtension: true
23495
23496
  },
23496
23497
  reasoning: {
23497
23498
  supported: false,
@@ -25174,7 +25175,9 @@ var AnthropicAdapter = class {
25174
25175
  if (req.thinking.mode === "off") {
25175
25176
  } else {
25176
25177
  const budget = req.thinking.effort ? ANTHROPIC_THINKING_BUDGETS[req.thinking.effort] ?? DEFAULT_ANTHROPIC_THINKING_BUDGET : DEFAULT_ANTHROPIC_THINKING_BUDGET;
25177
- body.thinking = { type: "enabled", budget_tokens: budget };
25178
+ const thinking = { type: "enabled", budget_tokens: budget };
25179
+ if (req.thinking.visibility === "hidden") thinking.display = "omitted";
25180
+ body.thinking = thinking;
25178
25181
  if (body.max_tokens <= budget) body.max_tokens = budget + 1024;
25179
25182
  }
25180
25183
  }
@@ -25691,6 +25694,21 @@ var GOOGLE_THINKING_LEVELS = {
25691
25694
  high: "HIGH",
25692
25695
  max: "HIGH"
25693
25696
  };
25697
+ var GOOGLE_THINKING_BUDGETS = {
25698
+ low: 2048,
25699
+ medium: 8192,
25700
+ high: 16384,
25701
+ max: 24576
25702
+ };
25703
+ function googleUsesThinkingBudget(model) {
25704
+ return /gemini-2\.5/.test(model);
25705
+ }
25706
+ var GOOGLE_INTERACTION_THINKING_LEVELS = {
25707
+ low: "low",
25708
+ medium: "medium",
25709
+ high: "high",
25710
+ max: "high"
25711
+ };
25694
25712
 
25695
25713
  // src/llm/providers/google/generate.ts
25696
25714
  var GoogleAdapter = class {
@@ -25771,9 +25789,16 @@ var GoogleAdapter = class {
25771
25789
  body.toolConfig = { functionCallingConfig: { mode } };
25772
25790
  }
25773
25791
  if (req.thinking && req.thinking.mode !== "off") {
25774
- config.thinkingConfig = {
25775
- thinkingLevel: GOOGLE_THINKING_LEVELS[req.thinking.effort ?? "high"] ?? "HIGH"
25792
+ const effort = req.thinking.effort ?? "high";
25793
+ const thinkingConfig = {
25794
+ includeThoughts: req.thinking.visibility !== "hidden"
25776
25795
  };
25796
+ if (googleUsesThinkingBudget(req.model)) {
25797
+ thinkingConfig.thinkingBudget = GOOGLE_THINKING_BUDGETS[effort] ?? GOOGLE_THINKING_BUDGETS.high;
25798
+ } else {
25799
+ thinkingConfig.thinkingLevel = GOOGLE_THINKING_LEVELS[effort] ?? "HIGH";
25800
+ }
25801
+ config.thinkingConfig = thinkingConfig;
25777
25802
  }
25778
25803
  if (req.structured) {
25779
25804
  config.responseMimeType = "application/json";
@@ -25789,6 +25814,9 @@ var GoogleAdapter = class {
25789
25814
  if (req.providerOptions.imageConfig) {
25790
25815
  config.imageConfig = req.providerOptions.imageConfig;
25791
25816
  }
25817
+ if (req.providerOptions.translationConfig) {
25818
+ config.translationConfig = req.providerOptions.translationConfig;
25819
+ }
25792
25820
  }
25793
25821
  return {
25794
25822
  body,
@@ -26139,8 +26167,6 @@ var GoogleInteractionsAdapter = class {
26139
26167
  if (req.maxTokens) genConfig.max_output_tokens = req.maxTokens;
26140
26168
  if (req.temperature !== void 0) genConfig.temperature = req.temperature;
26141
26169
  if (req.topP !== void 0) genConfig.top_p = req.topP;
26142
- if (req.presencePenalty !== void 0) genConfig.presence_penalty = req.presencePenalty;
26143
- if (req.frequencyPenalty !== void 0) genConfig.frequency_penalty = req.frequencyPenalty;
26144
26170
  if (req.stop) genConfig.stop_sequences = req.stop;
26145
26171
  if (req.tools?.length) {
26146
26172
  body.tools = req.tools.filter(isFunctionTool).map((t) => ({
@@ -26151,9 +26177,7 @@ var GoogleInteractionsAdapter = class {
26151
26177
  }));
26152
26178
  }
26153
26179
  if (req.thinking && req.thinking.mode !== "off") {
26154
- genConfig.thinking_config = {
26155
- thinking_level: GOOGLE_THINKING_LEVELS[req.thinking.effort ?? "high"] ?? "HIGH"
26156
- };
26180
+ genConfig.thinking_level = GOOGLE_INTERACTION_THINKING_LEVELS[req.thinking.effort ?? "high"] ?? "high";
26157
26181
  }
26158
26182
  if (Object.keys(genConfig).length > 0) body.generation_config = genConfig;
26159
26183
  const cachedContent = req.providerOptions?.cachedContent;
@@ -26456,6 +26480,24 @@ function openaiImageRef(ref) {
26456
26480
  function xaiImageRef(ref) {
26457
26481
  return ref.fileId ? { file_id: ref.fileId } : { url: toDataUrl(ref) };
26458
26482
  }
26483
+ function xaiVideoRef(src) {
26484
+ switch (src.type) {
26485
+ case "url":
26486
+ return { url: src.url };
26487
+ case "file":
26488
+ return { file_id: src.fileId };
26489
+ case "provider_ref":
26490
+ return { file_id: src.refId };
26491
+ case "base64":
26492
+ return { url: `data:${src.mimeType};base64,${src.data}` };
26493
+ case "buffer":
26494
+ return { url: `data:${src.mimeType};base64,${bytesToBase64(src.data)}` };
26495
+ case "path":
26496
+ throw new Error(
26497
+ "media source video: `path` DataSource is not supported here \u2014 read the file and pass base64/buffer."
26498
+ );
26499
+ }
26500
+ }
26459
26501
  function googleImagePart(ref) {
26460
26502
  const mimeType = ref.mimeType ?? "image/png";
26461
26503
  if (ref.base64) return { inline_data: { mime_type: mimeType, data: ref.base64 } };
@@ -26606,7 +26648,7 @@ var GoogleMediaAdapter = class {
26606
26648
  const image = {};
26607
26649
  if (req.params?.aspectRatio) image.aspectRatio = req.params.aspectRatio;
26608
26650
  if (req.params?.imageSize) image.imageSize = req.params.imageSize;
26609
- if (Object.keys(image).length) generationConfig.responseFormat = { image };
26651
+ if (Object.keys(image).length) generationConfig.imageConfig = image;
26610
26652
  const imagePart = googleImagePart(normalizeImageSource(req.sourceImage));
26611
26653
  const { items, usage } = await this.generateContentMedia(
26612
26654
  model,
@@ -27010,8 +27052,10 @@ var OpenAIBatchAdapter = class {
27010
27052
  };
27011
27053
 
27012
27054
  // src/llm/moderation/native.ts
27013
- function buildNativeModeration(mod) {
27014
- return { model: mod.model ?? MODERATION_DEFAULT_MODEL };
27055
+ function buildNativeModeration(mod, policy) {
27056
+ const out = { model: mod?.model ?? MODERATION_DEFAULT_MODEL };
27057
+ if (policy && typeof policy === "object") out.policy = policy;
27058
+ return out;
27015
27059
  }
27016
27060
  function parseNativeModeration(raw) {
27017
27061
  if (!raw || typeof raw !== "object") return void 0;
@@ -27121,8 +27165,12 @@ var OpenAIAdapter = class {
27121
27165
  if (req.stop) body.stop = req.stop;
27122
27166
  const tier = openaiRequestTier(req.serviceTier);
27123
27167
  if (tier) body.service_tier = tier;
27124
- if (req.moderation && req.moderation.mode !== "emulate") {
27125
- body.moderation = buildNativeModeration(req.moderation);
27168
+ const modPolicy = req.providerOptions?.moderationPolicy;
27169
+ if (req.moderation && req.moderation.mode !== "emulate" || modPolicy) {
27170
+ body.moderation = buildNativeModeration(req.moderation, modPolicy);
27171
+ }
27172
+ if (this.name === "openai" && req.providerOptions?.promptCacheOptions) {
27173
+ body.prompt_cache_options = req.providerOptions.promptCacheOptions;
27126
27174
  }
27127
27175
  const hasAudioInput = req.messages.some(
27128
27176
  (m) => Array.isArray(m.content) && m.content.some((p) => p.type === "audio")
@@ -27298,7 +27346,7 @@ var OpenAIAdapter = class {
27298
27346
  raw
27299
27347
  };
27300
27348
  }
27301
- parseStreamEvent(event) {
27349
+ parseStreamEvent(event, state) {
27302
27350
  const data = JSON.parse(event.data);
27303
27351
  if (data.moderation) {
27304
27352
  const report = parseNativeModeration(data.moderation);
@@ -27324,29 +27372,32 @@ var OpenAIAdapter = class {
27324
27372
  if (delta.content) {
27325
27373
  events.push({ type: "text", text: delta.content });
27326
27374
  }
27375
+ const toolIdByIndex = state?.toolIdByIndex ?? /* @__PURE__ */ new Map();
27327
27376
  const toolCalls = delta.tool_calls ?? [];
27328
27377
  for (const tc of toolCalls) {
27378
+ const index = tc.index ?? 0;
27379
+ let id = toolIdByIndex.get(index);
27380
+ if (id === void 0) {
27381
+ id = tc.id || `call_${crypto.randomUUID()}`;
27382
+ toolIdByIndex.set(index, id);
27383
+ }
27329
27384
  const fn = tc.function;
27330
27385
  if (fn?.name) {
27331
- events.push({
27332
- type: "tool_call_start",
27333
- id: tc.id ?? "",
27334
- name: fn.name
27335
- });
27386
+ events.push({ type: "tool_call_start", id, name: fn.name });
27336
27387
  }
27337
27388
  if (fn?.arguments) {
27338
- events.push({
27339
- type: "tool_call_delta",
27340
- id: tc.id ?? "",
27341
- arguments: fn.arguments
27342
- });
27389
+ events.push({ type: "tool_call_delta", id, arguments: fn.arguments });
27343
27390
  }
27344
27391
  }
27345
27392
  const fr = choice.finish_reason;
27346
27393
  if (fr) {
27347
27394
  events.push({
27348
27395
  type: "done",
27349
- finishReason: extractFinishReason(false, fr, { tool_calls: "tool_use", length: "length" })
27396
+ finishReason: extractFinishReason(false, fr, {
27397
+ tool_calls: "tool_use",
27398
+ length: "length",
27399
+ content_filter: "content_filter"
27400
+ })
27350
27401
  });
27351
27402
  }
27352
27403
  if (data.usage) {
@@ -27354,9 +27405,11 @@ var OpenAIAdapter = class {
27354
27405
  }
27355
27406
  return events;
27356
27407
  }
27357
- /** Stateless Chat Completions has no hosted code-execution file outputs. */
27408
+ /** Per-stream: correlates streamed tool-call fragments by index and synthesizes
27409
+ * a stable id for backends that omit tool-call ids (see `parseStreamEvent`). */
27358
27410
  createStreamParser() {
27359
- return (event) => this.parseStreamEvent(event);
27411
+ const state = { toolIdByIndex: /* @__PURE__ */ new Map() };
27412
+ return (event) => this.parseStreamEvent(event, state);
27360
27413
  }
27361
27414
  parseUsage(u) {
27362
27415
  if (!u) return emptyUsage();
@@ -27369,7 +27422,7 @@ var OpenAIAdapter = class {
27369
27422
  outputTokens: output,
27370
27423
  totalTokens: input + output,
27371
27424
  cachedTokens: details.cached_tokens ?? 0,
27372
- cacheWriteTokens: 0,
27425
+ cacheWriteTokens: details.cache_write_tokens ?? 0,
27373
27426
  reasoningTokens: outDetails.reasoning_tokens ?? 0
27374
27427
  };
27375
27428
  }
@@ -28015,8 +28068,12 @@ var OpenAIResponsesAdapter = class {
28015
28068
  if (req.topP !== void 0) body.top_p = req.topP;
28016
28069
  const tier = openaiRequestTier(req.serviceTier);
28017
28070
  if (tier) body.service_tier = tier;
28018
- if (req.moderation && req.moderation.mode !== "emulate") {
28019
- body.moderation = buildNativeModeration(req.moderation);
28071
+ const modPolicy = req.providerOptions?.moderationPolicy;
28072
+ if (req.moderation && req.moderation.mode !== "emulate" || modPolicy) {
28073
+ body.moderation = buildNativeModeration(req.moderation, modPolicy);
28074
+ }
28075
+ if (this.name === "openai" && req.providerOptions?.promptCacheOptions) {
28076
+ body.prompt_cache_options = req.providerOptions.promptCacheOptions;
28020
28077
  }
28021
28078
  if (req.tools?.length) {
28022
28079
  body.tools = req.tools.map((t) => {
@@ -28026,7 +28083,10 @@ var OpenAIResponsesAdapter = class {
28026
28083
  name: t.name,
28027
28084
  description: t.description,
28028
28085
  parameters: ensureAdditionalProperties(t.parameters),
28029
- strict: t.strict ?? true
28086
+ strict: t.strict ?? true,
28087
+ // Programmatic tool calling (Responses): who may call it + return schema.
28088
+ ...t.allowedCallers ? { allowed_callers: t.allowedCallers } : {},
28089
+ ...t.outputSchema ? { output_schema: t.outputSchema } : {}
28030
28090
  };
28031
28091
  }
28032
28092
  const builtin = { type: t.type, ...t.params };
@@ -28054,9 +28114,13 @@ var OpenAIResponsesAdapter = class {
28054
28114
  };
28055
28115
  }
28056
28116
  if (req.thinking && req.thinking.mode !== "off") {
28117
+ const visibility = req.thinking.visibility ?? "full";
28118
+ const summary = visibility === "hidden" ? null : visibility === "summary" ? "concise" : "auto";
28119
+ const mode = req.providerOptions?.reasoningMode;
28057
28120
  body.reasoning = {
28058
28121
  effort: req.thinking.effort ?? "medium",
28059
- summary: "auto",
28122
+ ...summary !== null ? { summary } : {},
28123
+ ...mode ? { mode } : {},
28060
28124
  // Cross-turn reasoning persistence (gpt-5/o-series, Responses only).
28061
28125
  ...req.thinking.context ? { context: req.thinking.context } : {}
28062
28126
  };
@@ -28200,9 +28264,8 @@ var OpenAIResponsesAdapter = class {
28200
28264
  }
28201
28265
  }
28202
28266
  const status = r.status;
28203
- const finishReason = extractFinishReason(toolCalls.length > 0, status, {
28204
- incomplete: "length"
28205
- });
28267
+ const incompleteReason = r.incomplete_details?.reason;
28268
+ const finishReason = incompleteReason === "content_filter" ? "content_filter" : extractFinishReason(toolCalls.length > 0, status, { incomplete: "length" });
28206
28269
  if (!text && typeof r.output_text === "string") {
28207
28270
  text = r.output_text;
28208
28271
  if (text && content.length === 0) content.push({ type: "text", text });
@@ -28322,7 +28385,7 @@ var OpenAIResponsesAdapter = class {
28322
28385
  outputTokens: output,
28323
28386
  totalTokens: u.total_tokens ?? input + output,
28324
28387
  cachedTokens: inputDetails.cached_tokens ?? 0,
28325
- cacheWriteTokens: 0,
28388
+ cacheWriteTokens: inputDetails.cache_write_tokens ?? 0,
28326
28389
  reasoningTokens: outputDetails.reasoning_tokens ?? 0
28327
28390
  };
28328
28391
  }
@@ -28412,8 +28475,9 @@ var OpenRouterAdapter = class extends OpenAIAdapter {
28412
28475
  * `url_citation` annotations appear in the stream (the `:online` search signal). */
28413
28476
  createStreamParser() {
28414
28477
  let webSearchEmitted = false;
28478
+ const state = { toolIdByIndex: /* @__PURE__ */ new Map() };
28415
28479
  return (event) => {
28416
- const events = this.parseStreamEvent(event);
28480
+ const events = this.parseStreamEvent(event, state);
28417
28481
  if (!webSearchEmitted) {
28418
28482
  const choice = JSON.parse(event.data).choices?.[0];
28419
28483
  const annotations = choice?.delta?.annotations ?? choice?.message?.annotations;
@@ -28584,8 +28648,8 @@ var XAIAdapter = class extends OpenAIAdapter {
28584
28648
  }
28585
28649
  return result;
28586
28650
  }
28587
- parseStreamEvent(event) {
28588
- const events = super.parseStreamEvent(event);
28651
+ parseStreamEvent(event, state) {
28652
+ const events = super.parseStreamEvent(event, state);
28589
28653
  try {
28590
28654
  const data = JSON.parse(event.data);
28591
28655
  const choices = data.choices ?? [];
@@ -28708,7 +28772,8 @@ var XAIMediaAdapter = class {
28708
28772
  imageEditing: true,
28709
28773
  audioGeneration: true,
28710
28774
  videoGeneration: true,
28711
- audioStreaming: true
28775
+ audioStreaming: true,
28776
+ videoExtension: true
28712
28777
  };
28713
28778
  }
28714
28779
  authHeaders() {
@@ -28822,13 +28887,9 @@ var XAIMediaAdapter = class {
28822
28887
  }
28823
28888
  async submitVideo(req, fetch2) {
28824
28889
  const model = req.model ?? "grok-imagine-video";
28825
- const body = { model, prompt: req.prompt };
28826
- if (req.params?.duration) body.duration = req.params.duration;
28827
- if (req.params?.aspectRatio) body.aspect_ratio = req.params.aspectRatio;
28828
- if (req.params?.resolution) body.resolution = req.params.resolution;
28829
- if (req.sourceImage) body.image = xaiImageRef(normalizeImageSource(req.sourceImage));
28890
+ const { url, body } = this.buildVideoSubmit(req, model);
28830
28891
  const res = await fetch2({
28831
- url: `${this.baseURL}/v1/videos/generations`,
28892
+ url,
28832
28893
  method: "POST",
28833
28894
  headers: this.authHeaders(),
28834
28895
  body,
@@ -28839,6 +28900,30 @@ var XAIMediaAdapter = class {
28839
28900
  const data = res.body;
28840
28901
  return data.request_id ?? data.id ?? "";
28841
28902
  }
28903
+ /** Route a video request to the right xAI endpoint by input + mode:
28904
+ * - no `sourceVideo` → `/v1/videos/generations` (text/image-to-video)
28905
+ * - `sourceVideo` + `videoMode:'extend'` (default) → `/v1/videos/extensions`
28906
+ * — continues from the last frame; takes `duration`, NOT aspect/resolution.
28907
+ * - `sourceVideo` + `videoMode:'edit'` → `/v1/videos/edits` — prompt + video
28908
+ * only (no duration/aspect/resolution).
28909
+ * All three return a `request_id` polled via the same status endpoint. */
28910
+ buildVideoSubmit(req, model) {
28911
+ if (req.sourceVideo) {
28912
+ const video = xaiVideoRef(req.sourceVideo);
28913
+ if ((req.params?.videoMode ?? "extend") === "edit") {
28914
+ return { url: `${this.baseURL}/v1/videos/edits`, body: { model, prompt: req.prompt, video } };
28915
+ }
28916
+ const body2 = { model, prompt: req.prompt, video };
28917
+ if (req.params?.duration) body2.duration = req.params.duration;
28918
+ return { url: `${this.baseURL}/v1/videos/extensions`, body: body2 };
28919
+ }
28920
+ const body = { model, prompt: req.prompt };
28921
+ if (req.params?.duration) body.duration = req.params.duration;
28922
+ if (req.params?.aspectRatio) body.aspect_ratio = req.params.aspectRatio;
28923
+ if (req.params?.resolution) body.resolution = req.params.resolution;
28924
+ if (req.sourceImage) body.image = xaiImageRef(normalizeImageSource(req.sourceImage));
28925
+ return { url: `${this.baseURL}/v1/videos/generations`, body };
28926
+ }
28842
28927
  async getVideoStatus(operationId, fetch2) {
28843
28928
  const res = await fetch2({
28844
28929
  url: `${this.baseURL}/v1/videos/${operationId}`,
@@ -28852,13 +28937,15 @@ var XAIMediaAdapter = class {
28852
28937
  if (res.status >= 400) return { status: "failed", error: `HTTP ${res.status}` };
28853
28938
  const data = res.body;
28854
28939
  const state = data.status ?? "";
28855
- if (state === "completed" || state === "ready" || data.download_url) {
28856
- return { status: "completed" };
28940
+ const video = data.video;
28941
+ const progress = data.progress;
28942
+ if (state === "done" || state === "completed" || state === "ready" || video?.url || data.download_url) {
28943
+ return { status: "completed", progress };
28857
28944
  }
28858
- if (state === "failed" || state === "error") {
28945
+ if (state === "failed" || state === "error" || state === "expired") {
28859
28946
  return { status: "failed", error: data.error ?? "Unknown error" };
28860
28947
  }
28861
- return { status: "processing", progress: data.progress };
28948
+ return { status: "processing", progress };
28862
28949
  }
28863
28950
  async downloadVideo(operationId, fetch2) {
28864
28951
  const statusRes = await fetch2({
@@ -28874,8 +28961,19 @@ var XAIMediaAdapter = class {
28874
28961
  throw new Error(`xAI video download failed: HTTP ${statusRes.status}`);
28875
28962
  }
28876
28963
  const data = statusRes.body;
28877
- const downloadUrl = data.download_url ?? data.url;
28964
+ const video = data.video;
28965
+ const downloadUrl = video?.url ?? data.download_url ?? data.url;
28878
28966
  if (!downloadUrl) throw new Error("No download URL in video response");
28967
+ const durationSec = video?.duration ?? data.duration;
28968
+ const base = {
28969
+ data: new Uint8Array(0),
28970
+ mimeType: "video/mp4",
28971
+ sourceUrl: downloadUrl,
28972
+ durationMs: durationSec ? durationSec * 1e3 : void 0,
28973
+ // Provider-reported cost (usage.cost_in_usd_ticks), when present.
28974
+ providerMeta: data.usage ? { usage: data.usage } : void 0
28975
+ };
28976
+ if (isBrowser()) return base;
28879
28977
  const videoRes = await fetch2({
28880
28978
  url: downloadUrl,
28881
28979
  method: "GET",
@@ -28885,13 +28983,7 @@ var XAIMediaAdapter = class {
28885
28983
  model: "",
28886
28984
  responseType: "arraybuffer"
28887
28985
  });
28888
- return {
28889
- data: videoRes.body,
28890
- mimeType: "video/mp4",
28891
- durationMs: data.duration ? data.duration * 1e3 : void 0,
28892
- // Provider-reported cost (usage.cost_in_usd_ticks), when present.
28893
- providerMeta: data.usage ? { usage: data.usage } : void 0
28894
- };
28986
+ return { ...base, data: videoRes.body };
28895
28987
  }
28896
28988
  async cancelVideo(operationId, fetch2) {
28897
28989
  await fetch2({
@@ -30304,7 +30396,11 @@ var AgentLoop = class _AgentLoop {
30304
30396
  id: lastResponse?.id ?? `agent-${runId}`,
30305
30397
  model: this.client.model,
30306
30398
  content: finalContent,
30307
- finishReason: reason === "done" ? "stop" : reason === "stopped" ? "stop" : reason === "guardrail" ? "stop" : reason === "max_steps" ? "length" : "error",
30399
+ finishReason: reason === "done" ? (
30400
+ // Ended because the model requested no tools — surface the provider's
30401
+ // actual reason (stop / content_filter / length), not a flat 'stop'.
30402
+ lastResponse?.finishReason ?? "stop"
30403
+ ) : reason === "stopped" ? "stop" : reason === "guardrail" ? "stop" : reason === "max_steps" ? "length" : "error",
30308
30404
  usage: totalUsage,
30309
30405
  text: finalText,
30310
30406
  toolCalls: lastResponse?.toolCalls ?? [],
@@ -30513,7 +30609,11 @@ var AgentLoop = class _AgentLoop {
30513
30609
  id: lastResponse?.id ?? `agent-${runId}`,
30514
30610
  model: this.client.model,
30515
30611
  content: finalContent,
30516
- finishReason: reason === "done" ? "stop" : reason === "stopped" ? "stop" : reason === "guardrail" ? "stop" : reason === "max_steps" ? "length" : "error",
30612
+ finishReason: reason === "done" ? (
30613
+ // Ended because the model requested no tools — surface the provider's
30614
+ // actual reason (stop / content_filter / length), not a flat 'stop'.
30615
+ lastResponse?.finishReason ?? "stop"
30616
+ ) : reason === "stopped" ? "stop" : reason === "guardrail" ? "stop" : reason === "max_steps" ? "length" : "error",
30517
30617
  usage: totalUsage,
30518
30618
  text: finalText,
30519
30619
  toolCalls: lastResponse?.toolCalls ?? [],
@@ -34838,9 +34938,13 @@ var MediaOutput = class {
34838
34938
  }
34839
34939
  async generateVideo(req) {
34840
34940
  const adapter = this.getAdapter(req.provider);
34841
- if (!adapter.capabilities().videoGeneration || !adapter.submitVideo) {
34941
+ const caps = adapter.capabilities();
34942
+ if (!caps.videoGeneration || !adapter.submitVideo) {
34842
34943
  throw new Error(`Provider ${req.provider} does not support video generation`);
34843
34944
  }
34945
+ if (req.sourceVideo && !caps.videoExtension) {
34946
+ throw new Error(`Provider ${req.provider} does not support video extension/editing`);
34947
+ }
34844
34948
  const { trace, fetch: fetch2 } = this.tracedOp();
34845
34949
  const operationId = await adapter.submitVideo(req, fetch2);
34846
34950
  return this.pollVideoCompletion(adapter, operationId, req, fetch2, trace);
@@ -34873,7 +34977,8 @@ var MediaOutput = class {
34873
34977
  width: raw.width,
34874
34978
  height: raw.height,
34875
34979
  durationMs: raw.durationMs,
34876
- sampleRate: raw.sampleRate
34980
+ sampleRate: raw.sampleRate,
34981
+ sourceUrl: raw.sourceUrl
34877
34982
  };
34878
34983
  await this.mediaStore.save(id, raw.data, meta);
34879
34984
  results.push({ id, type, mimeType: raw.mimeType, meta });
@@ -34906,6 +35011,15 @@ var MediaOutput = class {
34906
35011
  const start = Date.now();
34907
35012
  while (Date.now() - start < this.maxPollWaitMs) {
34908
35013
  const status = await adapter.getVideoStatus(operationId, fetch2);
35014
+ if (status.status === "processing" || status.status === "pending") {
35015
+ await this.hooks.emit("onMediaProgress", {
35016
+ type: "video",
35017
+ provider: req.provider,
35018
+ operationId,
35019
+ progress: status.progress,
35020
+ model: req.model
35021
+ });
35022
+ }
34909
35023
  if (status.status === "completed") {
34910
35024
  const raw = await adapter.downloadVideo(operationId, fetch2);
34911
35025
  const results = await this.saveResults(