@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 +88 -0
- package/dist/bus/hook-map.d.ts +11 -0
- package/dist/index.browser.js +177 -63
- package/dist/index.js +177 -63
- package/dist/llm/moderation/native.d.ts +5 -4
- package/dist/llm/providers/google/constants.d.ts +17 -2
- package/dist/llm/providers/openai/completions.d.ts +9 -2
- package/dist/llm/providers/xai/completions.d.ts +2 -2
- package/dist/llm/providers/xai/media.d.ts +8 -0
- package/dist/llm/types/request.d.ts +8 -0
- package/dist/llm/types/tools.d.ts +10 -1
- package/dist/plugins/media/source-image.d.ts +9 -0
- package/dist/plugins/media/types.d.ts +21 -0
- package/dist/plugins/model-catalog/catalog.d.ts +3 -0
- package/package.json +1 -1
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
|
package/dist/bus/hook-map.d.ts
CHANGED
|
@@ -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;
|
package/dist/index.browser.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
25775
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
27125
|
-
|
|
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, {
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
28019
|
-
|
|
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:
|
|
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
|
|
28204
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
28856
|
-
|
|
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
|
|
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
|
|
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" ?
|
|
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" ?
|
|
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
|
-
|
|
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(
|