@warlock.js/ai-openai 5.30.0 → 5.32.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 +42 -1
- package/README.md +3 -3
- package/cjs/index.cjs +715 -51
- package/cjs/index.cjs.map +1 -1
- package/esm/config.type.d.mts +13 -13
- package/esm/config.type.d.mts.map +1 -1
- package/esm/embedder.d.mts +1 -3
- package/esm/embedder.d.mts.map +1 -1
- package/esm/embedder.mjs.map +1 -1
- package/esm/image.d.mts +1 -3
- package/esm/image.d.mts.map +1 -1
- package/esm/index.d.mts +4 -1
- package/esm/index.mjs +3 -1
- package/esm/kimi-tools.d.mts +69 -0
- package/esm/kimi-tools.d.mts.map +1 -0
- package/esm/kimi-tools.mjs +256 -0
- package/esm/kimi-tools.mjs.map +1 -0
- package/esm/known-pricing.d.mts +39 -0
- package/esm/known-pricing.d.mts.map +1 -0
- package/esm/known-pricing.mjs +121 -0
- package/esm/known-pricing.mjs.map +1 -0
- package/esm/model.mjs +82 -23
- package/esm/model.mjs.map +1 -1
- package/esm/provider-options.d.mts +13 -0
- package/esm/provider-options.d.mts.map +1 -0
- package/esm/provider-options.mjs +56 -0
- package/esm/provider-options.mjs.map +1 -0
- package/esm/responses-model.mjs +125 -19
- package/esm/responses-model.mjs.map +1 -1
- package/esm/sdk.d.mts +4 -5
- package/esm/sdk.d.mts.map +1 -1
- package/esm/sdk.mjs +5 -3
- package/esm/sdk.mjs.map +1 -1
- package/esm/speech.d.mts +1 -3
- package/esm/speech.d.mts.map +1 -1
- package/esm/transcription.d.mts +1 -3
- package/esm/transcription.d.mts.map +1 -1
- package/esm/transcription.mjs.map +1 -1
- package/esm/utils/build-usage.mjs +14 -2
- package/esm/utils/build-usage.mjs.map +1 -1
- package/esm/utils/hosted-output.mjs +22 -0
- package/esm/utils/hosted-output.mjs.map +1 -0
- package/esm/utils/index.mjs +2 -2
- package/esm/utils/map-responses-status.mjs.map +1 -1
- package/esm/utils/prompt-cache-params.mjs +25 -6
- package/esm/utils/prompt-cache-params.mjs.map +1 -1
- package/esm/utils/reasoning-replay.mjs +23 -1
- package/esm/utils/reasoning-replay.mjs.map +1 -1
- package/esm/utils/structured-output.mjs +1 -1
- package/esm/utils/structured-output.mjs.map +1 -1
- package/esm/utils/to-openai-messages.mjs.map +1 -1
- package/esm/utils/to-openai-tools.mjs.map +1 -1
- package/esm/utils/wrap-openai-error.mjs +6 -1
- package/esm/utils/wrap-openai-error.mjs.map +1 -1
- package/llms-full.txt +235 -37
- package/llms.txt +1 -1
- package/package.json +3 -3
- package/skills/setup-openai/SKILL.md +235 -37
package/cjs/index.cjs
CHANGED
|
@@ -20,7 +20,7 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
20
20
|
}
|
|
21
21
|
return to;
|
|
22
22
|
};
|
|
23
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
23
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
|
24
24
|
value: mod,
|
|
25
25
|
enumerable: true
|
|
26
26
|
}) : target, mod));
|
|
@@ -31,6 +31,125 @@ openai = __toESM(openai, 1);
|
|
|
31
31
|
let _warlock_js_ai = require("@warlock.js/ai");
|
|
32
32
|
let _warlock_js_logger = require("@warlock.js/logger");
|
|
33
33
|
|
|
34
|
+
//#region ../../packages/ai-openai/src/known-pricing.ts
|
|
35
|
+
/**
|
|
36
|
+
* Built-in OpenAI API list prices, USD per 1,000,000 tokens, keyed by
|
|
37
|
+
* exact model id. `cachedInput` is the cached-input rate. None of these
|
|
38
|
+
* models charges for cache writes.
|
|
39
|
+
*
|
|
40
|
+
* Source: https://developers.openai.com/api/docs/pricing (the target of
|
|
41
|
+
* platform.openai.com/docs/pricing), Standard tier, fetched 2026-10-17.
|
|
42
|
+
*
|
|
43
|
+
* Applied only when the SDK's provider label is `"openai"`, and only on
|
|
44
|
+
* an exact id match — dated snapshots can carry other prices. Left out on
|
|
45
|
+
* purpose, so pass `pricing` yourself for them:
|
|
46
|
+
*
|
|
47
|
+
* - Models priced by prompt length (a higher rate above 272K input
|
|
48
|
+
* tokens): the gpt-6 / gpt-5.6 / gpt-5.5 / gpt-5.4 families and the
|
|
49
|
+
* `-pro` models. A flat `ModelPricing` cannot express the tiers.
|
|
50
|
+
* - Promotional, Batch, Flex and Priority pricing, and the regional
|
|
51
|
+
* (data-residency) uplift.
|
|
52
|
+
*/
|
|
53
|
+
const OPENAI_MODEL_PRICING = {
|
|
54
|
+
"gpt-5.4-mini": {
|
|
55
|
+
input: .75,
|
|
56
|
+
output: 4.5,
|
|
57
|
+
cachedInput: .075
|
|
58
|
+
},
|
|
59
|
+
"gpt-5.4-nano": {
|
|
60
|
+
input: .2,
|
|
61
|
+
output: 1.25,
|
|
62
|
+
cachedInput: .02
|
|
63
|
+
},
|
|
64
|
+
"gpt-5.2": {
|
|
65
|
+
input: 1.75,
|
|
66
|
+
output: 14,
|
|
67
|
+
cachedInput: .175
|
|
68
|
+
},
|
|
69
|
+
"gpt-5.1": {
|
|
70
|
+
input: 1.25,
|
|
71
|
+
output: 10,
|
|
72
|
+
cachedInput: .125
|
|
73
|
+
},
|
|
74
|
+
"gpt-5": {
|
|
75
|
+
input: 1.25,
|
|
76
|
+
output: 10,
|
|
77
|
+
cachedInput: .125
|
|
78
|
+
},
|
|
79
|
+
"gpt-5-mini": {
|
|
80
|
+
input: .25,
|
|
81
|
+
output: 2,
|
|
82
|
+
cachedInput: .025
|
|
83
|
+
},
|
|
84
|
+
"gpt-5-nano": {
|
|
85
|
+
input: .05,
|
|
86
|
+
output: .4,
|
|
87
|
+
cachedInput: .005
|
|
88
|
+
},
|
|
89
|
+
"gpt-4.1": {
|
|
90
|
+
input: 2,
|
|
91
|
+
output: 8,
|
|
92
|
+
cachedInput: .5
|
|
93
|
+
},
|
|
94
|
+
"gpt-4.1-mini": {
|
|
95
|
+
input: .4,
|
|
96
|
+
output: 1.6,
|
|
97
|
+
cachedInput: .1
|
|
98
|
+
},
|
|
99
|
+
"gpt-4.1-nano": {
|
|
100
|
+
input: .1,
|
|
101
|
+
output: .4,
|
|
102
|
+
cachedInput: .025
|
|
103
|
+
},
|
|
104
|
+
"gpt-4o": {
|
|
105
|
+
input: 2.5,
|
|
106
|
+
output: 10,
|
|
107
|
+
cachedInput: 1.25
|
|
108
|
+
},
|
|
109
|
+
"gpt-4o-mini": {
|
|
110
|
+
input: .15,
|
|
111
|
+
output: .6,
|
|
112
|
+
cachedInput: .075
|
|
113
|
+
},
|
|
114
|
+
o3: {
|
|
115
|
+
input: 2,
|
|
116
|
+
output: 8,
|
|
117
|
+
cachedInput: .5
|
|
118
|
+
},
|
|
119
|
+
"o4-mini": {
|
|
120
|
+
input: 1.1,
|
|
121
|
+
output: 4.4,
|
|
122
|
+
cachedInput: .275
|
|
123
|
+
},
|
|
124
|
+
"o3-mini": {
|
|
125
|
+
input: 1.1,
|
|
126
|
+
output: 4.4,
|
|
127
|
+
cachedInput: .55
|
|
128
|
+
},
|
|
129
|
+
o1: {
|
|
130
|
+
input: 15,
|
|
131
|
+
output: 60,
|
|
132
|
+
cachedInput: 7.5
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* Look up the built-in list price for an OpenAI model id (exact match).
|
|
137
|
+
* Returns `undefined` for unknown ids, so cost stays unknown rather than
|
|
138
|
+
* wrong.
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* openAIPricingFor("gpt-4o-mini"); // { input: 0.15, output: 0.6, cachedInput: 0.075 }
|
|
142
|
+
* openAIPricingFor("gpt-5.5"); // undefined — priced by prompt length
|
|
143
|
+
*/
|
|
144
|
+
function openAIPricingFor(modelName) {
|
|
145
|
+
return OPENAI_MODEL_PRICING[modelName];
|
|
146
|
+
}
|
|
147
|
+
/** USD per request for web_search (NOT non-reasoning web_search_preview).
|
|
148
|
+
* Search content tokens are additional; preview variants can have other rates.
|
|
149
|
+
* https://developers.openai.com/api/docs/pricing#built-in-tools — verified 2026-10-18. */
|
|
150
|
+
const OPENAI_SERVER_TOOL_PRICING = { webSearch: .01 };
|
|
151
|
+
|
|
152
|
+
//#endregion
|
|
34
153
|
//#region ../../packages/ai-openai/src/utils/build-usage.ts
|
|
35
154
|
/**
|
|
36
155
|
* Build the neutral `Usage` from extracted counts. The optional detail
|
|
@@ -38,17 +157,27 @@ let _warlock_js_logger = require("@warlock.js/logger");
|
|
|
38
157
|
* non-reasoning / uncached calls keep the lean `{ input, output, total }`
|
|
39
158
|
* shape. Shared by the Chat Completions and Responses adapters so both
|
|
40
159
|
* apply the same rule.
|
|
160
|
+
*
|
|
161
|
+
* OpenAI-style usage counts cache reads and writes inside the prompt
|
|
162
|
+
* total (`input_tokens` includes `cached_tokens` and `cache_write_tokens`,
|
|
163
|
+
* and the compatible vendors — DeepSeek, xAI, Groq, Mistral — document
|
|
164
|
+
* the same), so the result is marked `inputIncludesCache: true`. When the
|
|
165
|
+
* model is priced, `cost` is attached.
|
|
41
166
|
*/
|
|
42
|
-
function buildUsage(counts) {
|
|
167
|
+
function buildUsage(counts, pricing) {
|
|
43
168
|
const { input, output, total, cachedTokens, reasoningTokens, cacheWriteTokens } = counts;
|
|
44
|
-
|
|
169
|
+
const usage = {
|
|
45
170
|
input,
|
|
46
171
|
output,
|
|
47
172
|
total,
|
|
173
|
+
inputIncludesCache: true,
|
|
48
174
|
...cachedTokens !== void 0 && cachedTokens > 0 ? { cachedTokens } : {},
|
|
49
175
|
...reasoningTokens !== void 0 && reasoningTokens > 0 ? { reasoningTokens } : {},
|
|
50
176
|
...cacheWriteTokens !== void 0 && cacheWriteTokens > 0 ? { cacheWriteTokens } : {}
|
|
51
177
|
};
|
|
178
|
+
const cost = (0, _warlock_js_ai.computeCost)(usage, pricing);
|
|
179
|
+
if (cost) usage.cost = cost;
|
|
180
|
+
return usage;
|
|
52
181
|
}
|
|
53
182
|
|
|
54
183
|
//#endregion
|
|
@@ -109,6 +238,11 @@ function wrapOpenAIError(thrown) {
|
|
|
109
238
|
cause: thrown,
|
|
110
239
|
context
|
|
111
240
|
});
|
|
241
|
+
if (shape.code === "server_is_overloaded" || shape.status === 503) return new _warlock_js_ai.ProviderOverloadedError(message, {
|
|
242
|
+
cause: thrown,
|
|
243
|
+
context,
|
|
244
|
+
retryAfter: parseRetryAfter(shape.headers)
|
|
245
|
+
});
|
|
112
246
|
return new _warlock_js_ai.ProviderError(message, {
|
|
113
247
|
cause: thrown,
|
|
114
248
|
context
|
|
@@ -271,15 +405,34 @@ function mapFinishReason(raw) {
|
|
|
271
405
|
//#endregion
|
|
272
406
|
//#region ../../packages/ai-openai/src/utils/prompt-cache-params.ts
|
|
273
407
|
/**
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
408
|
+
* Provider labels whose OpenAI-compatible endpoint documents a
|
|
409
|
+
* `prompt_cache_key` request field:
|
|
410
|
+
*
|
|
411
|
+
* - `openai` — https://developers.openai.com/api/docs/guides/prompt-caching
|
|
412
|
+
* - `xai` — Chat Completions accepts `prompt_cache_key` ("plumbed to
|
|
413
|
+
* x-grok-conv-id"), https://docs.x.ai/openapi.json and
|
|
414
|
+
* https://docs.x.ai/developers/advanced-api-usage/prompt-caching/maximizing-cache-hits
|
|
415
|
+
* - `mistral` — https://docs.mistral.ai/studio/conversations/advanced/prompt-caching
|
|
416
|
+
*
|
|
417
|
+
* Groq and DeepSeek cache automatically and document no request hint, so
|
|
418
|
+
* they receive nothing (checked 2026-10-17).
|
|
419
|
+
*/
|
|
420
|
+
const PROMPT_CACHE_KEY_PROVIDERS = /* @__PURE__ */ new Set([
|
|
421
|
+
"openai",
|
|
422
|
+
"xai",
|
|
423
|
+
"mistral"
|
|
424
|
+
]);
|
|
425
|
+
/**
|
|
426
|
+
* Build the prompt-cache request fields from the model config.
|
|
427
|
+
* `prompt_cache_key` is sent for the providers that document it
|
|
428
|
+
* ({@link PROMPT_CACHE_KEY_PROVIDERS}); `prompt_cache_retention` is
|
|
429
|
+
* OpenAI-only. Any other provider label — including a custom label on a
|
|
430
|
+
* wrapper SDK — gets neither, since an unknown endpoint may reject them.
|
|
277
431
|
*/
|
|
278
432
|
function buildPromptCacheParams(provider, config) {
|
|
279
|
-
if (provider !== "openai") return {};
|
|
280
433
|
return {
|
|
281
|
-
...config.promptCacheKey !== void 0 ? { prompt_cache_key: config.promptCacheKey } : {},
|
|
282
|
-
...config.promptCacheRetention !== void 0 ? { prompt_cache_retention: config.promptCacheRetention } : {}
|
|
434
|
+
...PROMPT_CACHE_KEY_PROVIDERS.has(provider) && config.promptCacheKey !== void 0 ? { prompt_cache_key: config.promptCacheKey } : {},
|
|
435
|
+
...provider === "openai" && config.promptCacheRetention !== void 0 ? { prompt_cache_retention: config.promptCacheRetention } : {}
|
|
283
436
|
};
|
|
284
437
|
}
|
|
285
438
|
|
|
@@ -415,6 +568,28 @@ const RESPONSES_METADATA_KEY = "openaiResponses";
|
|
|
415
568
|
function pickReplayableReasoning(items) {
|
|
416
569
|
return items.filter((item) => typeof item.encrypted_content === "string" && item.encrypted_content !== "");
|
|
417
570
|
}
|
|
571
|
+
/** Output item types that may follow a replayed reasoning item. */
|
|
572
|
+
const REPLAY_SAFE_FOLLOWERS = /* @__PURE__ */ new Set(["function_call", "message"]);
|
|
573
|
+
/**
|
|
574
|
+
* The reasoning items of a turn that are safe to replay, in order.
|
|
575
|
+
*
|
|
576
|
+
* The API pairs a reasoning item with the output item that followed it and
|
|
577
|
+
* rejects a replayed reasoning item whose follower is missing. Only
|
|
578
|
+
* `function_call` items are replayed, so a reasoning item that led into a
|
|
579
|
+
* hosted (server-run) tool call — `web_search_call`, `file_search_call`,
|
|
580
|
+
* `code_interpreter_call`, … from a provider-defined tool — is dropped
|
|
581
|
+
* rather than sent without its follower. Encrypted reasoning only improves
|
|
582
|
+
* continuity; dropping it never breaks the request.
|
|
583
|
+
*/
|
|
584
|
+
function pickTurnReasoning(output) {
|
|
585
|
+
const picked = [];
|
|
586
|
+
output.forEach((item, index) => {
|
|
587
|
+
if (item.type !== "reasoning") return;
|
|
588
|
+
const follower = output.slice(index + 1).find((next) => next.type !== "reasoning");
|
|
589
|
+
if (follower === void 0 || REPLAY_SAFE_FOLLOWERS.has(follower.type)) picked.push(item);
|
|
590
|
+
});
|
|
591
|
+
return picked;
|
|
592
|
+
}
|
|
418
593
|
/**
|
|
419
594
|
* Attach the turn's reasoning items to the FIRST tool call only (parallel
|
|
420
595
|
* calls do not each repeat the blobs). Returns the calls unchanged when
|
|
@@ -1065,6 +1240,58 @@ var OpenAIImageModel = class {
|
|
|
1065
1240
|
}
|
|
1066
1241
|
};
|
|
1067
1242
|
|
|
1243
|
+
//#endregion
|
|
1244
|
+
//#region ../../packages/ai-openai/src/provider-options.ts
|
|
1245
|
+
const RESERVED_FIELDS = /* @__PURE__ */ new Set([
|
|
1246
|
+
"model",
|
|
1247
|
+
"messages",
|
|
1248
|
+
"input",
|
|
1249
|
+
"instructions",
|
|
1250
|
+
"tools",
|
|
1251
|
+
"stream",
|
|
1252
|
+
"stream_options",
|
|
1253
|
+
"previous_response_id",
|
|
1254
|
+
"conversation",
|
|
1255
|
+
"store",
|
|
1256
|
+
"include",
|
|
1257
|
+
"functions",
|
|
1258
|
+
"function_call",
|
|
1259
|
+
"prompt",
|
|
1260
|
+
"background"
|
|
1261
|
+
]);
|
|
1262
|
+
function mergeProviderBody(built, options) {
|
|
1263
|
+
const body = (options?.providerOptions?.openai)?.body ?? {};
|
|
1264
|
+
const reserved = Object.keys(body).filter((key) => RESERVED_FIELDS.has(key));
|
|
1265
|
+
if (reserved.length > 0) throw new _warlock_js_ai.InvalidRequestError(`providerOptions.openai.body cannot set ${reserved.join(", ")}: use the call's messages, tools and model options.`, { context: { fields: reserved } });
|
|
1266
|
+
const defaults = built;
|
|
1267
|
+
const overrides = Object.fromEntries(Object.entries(body).map(([key, value]) => {
|
|
1268
|
+
const previous = defaults[key];
|
|
1269
|
+
if (!isPlainObject(previous) || !isPlainObject(value)) return [key, value];
|
|
1270
|
+
if (typeof previous.type === "string" && typeof value.type === "string" && previous.type !== value.type) return [key, value];
|
|
1271
|
+
return [key, {
|
|
1272
|
+
...previous,
|
|
1273
|
+
...value
|
|
1274
|
+
}];
|
|
1275
|
+
}));
|
|
1276
|
+
return {
|
|
1277
|
+
...built,
|
|
1278
|
+
...overrides
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
/** Merge only plain objects; arrays, null and provider scalar values replace the field. */
|
|
1282
|
+
function isPlainObject(value) {
|
|
1283
|
+
if (value === null || typeof value !== "object") return false;
|
|
1284
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1285
|
+
return prototype === Object.prototype || prototype === null;
|
|
1286
|
+
}
|
|
1287
|
+
function compatibleSearchParams(provider, options) {
|
|
1288
|
+
if (provider !== "qwen" && provider !== "dashscope") return {};
|
|
1289
|
+
const value = options?.providerOptions?.[provider]?.enableSearch;
|
|
1290
|
+
if (value === void 0) return {};
|
|
1291
|
+
if (typeof value !== "boolean") throw new _warlock_js_ai.InvalidRequestError(`providerOptions.${provider}.enableSearch must be a boolean`);
|
|
1292
|
+
return { enable_search: value };
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1068
1295
|
//#endregion
|
|
1069
1296
|
//#region ../../packages/ai-openai/src/known-reasoning-models.ts
|
|
1070
1297
|
/**
|
|
@@ -1227,15 +1454,16 @@ var OpenAIModel = class {
|
|
|
1227
1454
|
});
|
|
1228
1455
|
let response;
|
|
1229
1456
|
try {
|
|
1230
|
-
response = await this.client.chat.completions.create({
|
|
1457
|
+
response = await this.client.chat.completions.create(mergeProviderBody({
|
|
1231
1458
|
model: this.name,
|
|
1459
|
+
...compatibleSearchParams(this.provider, options),
|
|
1232
1460
|
messages: toOpenAIMessages(messages, this.promptCacheBreakpoints(options)),
|
|
1233
1461
|
...this.buildSamplingParams(options),
|
|
1234
1462
|
...buildPromptCacheParams(this.provider, this.config),
|
|
1235
|
-
|
|
1463
|
+
...this.buildTools(options),
|
|
1236
1464
|
...this.buildResponseFormat(options?.responseSchema),
|
|
1237
1465
|
...this.buildReasoningParams(options?.reasoning, Boolean(options?.tools?.length))
|
|
1238
|
-
}, options?.signal ? { signal: options.signal } : void 0);
|
|
1466
|
+
}, options), options?.signal ? { signal: options.signal } : void 0);
|
|
1239
1467
|
} catch (thrown) {
|
|
1240
1468
|
const wrapped = wrapOpenAIError(thrown);
|
|
1241
1469
|
this.logger.error(LOG_MODULE$3, "error", wrapped.message, {
|
|
@@ -1268,6 +1496,11 @@ var OpenAIModel = class {
|
|
|
1268
1496
|
* `delta` for text tokens, `tool-call` when the model requests a tool,
|
|
1269
1497
|
* and a terminal `done` carrying the final finish reason + usage totals.
|
|
1270
1498
|
* Callers consume it with `for await`.
|
|
1499
|
+
*
|
|
1500
|
+
* With `options.streamChunks` it also yields `tool-args` for each
|
|
1501
|
+
* `tool_calls[].function.arguments` fragment and `reasoning` for a
|
|
1502
|
+
* vendor's `reasoning_content` / `reasoning` delta field. Usage arrives
|
|
1503
|
+
* only on the final chunk here, so no `usage` chunk is emitted.
|
|
1271
1504
|
*/
|
|
1272
1505
|
async *stream(messages, options) {
|
|
1273
1506
|
this.logger.debug(LOG_MODULE$3, "request", "Starting streaming call to chat.completions", {
|
|
@@ -1278,17 +1511,18 @@ var OpenAIModel = class {
|
|
|
1278
1511
|
});
|
|
1279
1512
|
let stream;
|
|
1280
1513
|
try {
|
|
1281
|
-
stream = await this.client.chat.completions.create({
|
|
1514
|
+
stream = await this.client.chat.completions.create(mergeProviderBody({
|
|
1282
1515
|
model: this.name,
|
|
1516
|
+
...compatibleSearchParams(this.provider, options),
|
|
1283
1517
|
messages: toOpenAIMessages(messages, this.promptCacheBreakpoints(options)),
|
|
1284
1518
|
...this.buildSamplingParams(options),
|
|
1285
1519
|
...buildPromptCacheParams(this.provider, this.config),
|
|
1286
|
-
|
|
1520
|
+
...this.buildTools(options),
|
|
1287
1521
|
stream: true,
|
|
1288
1522
|
stream_options: { include_usage: true },
|
|
1289
1523
|
...this.buildResponseFormat(options?.responseSchema),
|
|
1290
1524
|
...this.buildReasoningParams(options?.reasoning, Boolean(options?.tools?.length))
|
|
1291
|
-
}, options?.signal ? { signal: options.signal } : void 0);
|
|
1525
|
+
}, options), options?.signal ? { signal: options.signal } : void 0);
|
|
1292
1526
|
} catch (thrown) {
|
|
1293
1527
|
const wrapped = wrapOpenAIError(thrown);
|
|
1294
1528
|
this.logger.error(LOG_MODULE$3, "error", wrapped.message, {
|
|
@@ -1298,16 +1532,24 @@ var OpenAIModel = class {
|
|
|
1298
1532
|
throw wrapped;
|
|
1299
1533
|
}
|
|
1300
1534
|
let rawFinishReason = "stop";
|
|
1301
|
-
|
|
1535
|
+
let usage = {
|
|
1302
1536
|
input: 0,
|
|
1303
1537
|
output: 0,
|
|
1304
1538
|
total: 0
|
|
1305
1539
|
};
|
|
1540
|
+
const want = (0, _warlock_js_ai.resolveStreamChunks)(options);
|
|
1306
1541
|
const toolCallAccum = /* @__PURE__ */ new Map();
|
|
1307
1542
|
try {
|
|
1308
1543
|
for await (const chunk of stream) {
|
|
1309
1544
|
const delta = chunk.choices[0]?.delta;
|
|
1310
1545
|
const finish = chunk.choices[0]?.finish_reason;
|
|
1546
|
+
if (want.reasoning && delta) {
|
|
1547
|
+
const reasoning = readReasoningDelta(delta);
|
|
1548
|
+
if (reasoning) yield {
|
|
1549
|
+
type: "reasoning",
|
|
1550
|
+
delta: reasoning
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1311
1553
|
if (delta?.content) yield {
|
|
1312
1554
|
type: "delta",
|
|
1313
1555
|
content: delta.content
|
|
@@ -1317,23 +1559,25 @@ var OpenAIModel = class {
|
|
|
1317
1559
|
if (!toolCallAccum.has(idx)) toolCallAccum.set(idx, {
|
|
1318
1560
|
id: "",
|
|
1319
1561
|
name: "",
|
|
1320
|
-
arguments: ""
|
|
1562
|
+
arguments: "",
|
|
1563
|
+
streamed: 0
|
|
1321
1564
|
});
|
|
1322
1565
|
const acc = toolCallAccum.get(idx);
|
|
1323
1566
|
if (toolCall.id) acc.id = toolCall.id;
|
|
1324
1567
|
if (toolCall.function?.name) acc.name = toolCall.function.name;
|
|
1325
1568
|
if (toolCall.function?.arguments) acc.arguments += toolCall.function.arguments;
|
|
1569
|
+
if (want.toolArgs && acc.name && acc.arguments.length > acc.streamed) {
|
|
1570
|
+
yield {
|
|
1571
|
+
type: "tool-args",
|
|
1572
|
+
callId: acc.id,
|
|
1573
|
+
name: acc.name,
|
|
1574
|
+
delta: acc.arguments.slice(acc.streamed)
|
|
1575
|
+
};
|
|
1576
|
+
acc.streamed = acc.arguments.length;
|
|
1577
|
+
}
|
|
1326
1578
|
}
|
|
1327
1579
|
if (finish) rawFinishReason = finish;
|
|
1328
|
-
if (chunk.usage)
|
|
1329
|
-
usage.input = chunk.usage.prompt_tokens ?? 0;
|
|
1330
|
-
usage.output = chunk.usage.completion_tokens ?? 0;
|
|
1331
|
-
usage.total = chunk.usage.total_tokens ?? 0;
|
|
1332
|
-
const cached = chunk.usage.prompt_tokens_details?.cached_tokens;
|
|
1333
|
-
if (cached !== void 0 && cached > 0) usage.cachedTokens = cached;
|
|
1334
|
-
const reasoning = chunk.usage.completion_tokens_details?.reasoning_tokens;
|
|
1335
|
-
if (reasoning !== void 0 && reasoning > 0) usage.reasoningTokens = reasoning;
|
|
1336
|
-
}
|
|
1580
|
+
if (chunk.usage) usage = this.extractUsage(chunk.usage);
|
|
1337
1581
|
}
|
|
1338
1582
|
for (const acc of toolCallAccum.values()) {
|
|
1339
1583
|
if (!acc.name) continue;
|
|
@@ -1363,6 +1607,30 @@ var OpenAIModel = class {
|
|
|
1363
1607
|
usage
|
|
1364
1608
|
};
|
|
1365
1609
|
}
|
|
1610
|
+
/**
|
|
1611
|
+
* Spread-friendly `tools` fragment: the function tools, then the
|
|
1612
|
+
* provider-defined entries whose `provider` equals this model's
|
|
1613
|
+
* provider label (`"openai"`, or the vendor label an OpenAI-compatible
|
|
1614
|
+
* wrapper sets, e.g. `"groq"`), sent verbatim. Chat Completions only
|
|
1615
|
+
* accepts `function` and `custom` tool types from OpenAI itself; its
|
|
1616
|
+
* hosted tools (`web_search`, `file_search`, …) need `api: "responses"`
|
|
1617
|
+
* and OpenAI rejects them here with a 400.
|
|
1618
|
+
*/
|
|
1619
|
+
buildTools(options) {
|
|
1620
|
+
const { tools, definitions } = (0, _warlock_js_ai.selectProviderTools)(options, {
|
|
1621
|
+
accepts: [this.provider],
|
|
1622
|
+
module: LOG_MODULE$3,
|
|
1623
|
+
model: this.name,
|
|
1624
|
+
logger: this.logger
|
|
1625
|
+
});
|
|
1626
|
+
if ((this.provider === "moonshot" || this.provider === "kimi") && definitions.some((definition) => {
|
|
1627
|
+
const fn = definition.function;
|
|
1628
|
+
return definition.type === "builtin_function" && typeof fn === "object" && fn !== null && "name" in fn && fn.name === "$web_search";
|
|
1629
|
+
})) throw new _warlock_js_ai.InvalidRequestError("Kimi's builtin $web_search retires on 20 October 2026. Use kimiSearchTool() / kimiFetchTool() from @warlock.js/ai-openai.");
|
|
1630
|
+
if (this.provider === "xai" && definitions.some((definition) => definition.type === "web_search" || definition.type === "x_search")) throw new _warlock_js_ai.InvalidRequestError("xAI native web_search / x_search requires POST /v1/responses. XaiSDK currently supports Chat Completions only; use xAI's Responses API with the vendor SDK for native search.");
|
|
1631
|
+
const mapped = [...toOpenAITools(tools) ?? [], ...definitions];
|
|
1632
|
+
return mapped.length > 0 ? { tools: mapped } : {};
|
|
1633
|
+
}
|
|
1366
1634
|
/** Map the neutral cache hint only for the direct OpenAI provider. */
|
|
1367
1635
|
promptCacheBreakpoints(options) {
|
|
1368
1636
|
return this.provider === "openai" ? options?.cacheControl?.breakpoints ?? 0 : 0;
|
|
@@ -1401,7 +1669,9 @@ var OpenAIModel = class {
|
|
|
1401
1669
|
* (the hidden reasoning channel on o-series / gpt-5 models, already
|
|
1402
1670
|
* counted within `output`). Both are emitted only when the provider
|
|
1403
1671
|
* reports a positive value, so non-reasoning / uncached calls keep the
|
|
1404
|
-
* lean `{ input, output, total }` shape.
|
|
1672
|
+
* lean `{ input, output, total }` shape. `cacheWriteTokens` mirrors
|
|
1673
|
+
* `prompt_tokens_details.cache_write_tokens` (billed writes on the
|
|
1674
|
+
* models that charge for them). `cost` is attached when priced.
|
|
1405
1675
|
*/
|
|
1406
1676
|
extractUsage(raw) {
|
|
1407
1677
|
if (!raw) return {
|
|
@@ -1410,12 +1680,13 @@ var OpenAIModel = class {
|
|
|
1410
1680
|
total: 0
|
|
1411
1681
|
};
|
|
1412
1682
|
return buildUsage({
|
|
1413
|
-
input: raw.prompt_tokens,
|
|
1414
|
-
output: raw.completion_tokens,
|
|
1415
|
-
total: raw.total_tokens,
|
|
1683
|
+
input: raw.prompt_tokens ?? 0,
|
|
1684
|
+
output: raw.completion_tokens ?? 0,
|
|
1685
|
+
total: raw.total_tokens ?? 0,
|
|
1416
1686
|
cachedTokens: raw.prompt_tokens_details?.cached_tokens,
|
|
1687
|
+
cacheWriteTokens: raw.prompt_tokens_details?.cache_write_tokens,
|
|
1417
1688
|
reasoningTokens: raw.completion_tokens_details?.reasoning_tokens
|
|
1418
|
-
});
|
|
1689
|
+
}, this.pricing);
|
|
1419
1690
|
}
|
|
1420
1691
|
/**
|
|
1421
1692
|
* Translate the neutral `ModelCallOptions.reasoning` hint into OpenAI's
|
|
@@ -1491,11 +1762,56 @@ var OpenAIModel = class {
|
|
|
1491
1762
|
}));
|
|
1492
1763
|
}
|
|
1493
1764
|
};
|
|
1765
|
+
/**
|
|
1766
|
+
* Reasoning text an OpenAI-compatible vendor puts on a Chat Completions
|
|
1767
|
+
* stream delta. OpenAI itself streams none on this API. DeepSeek documents
|
|
1768
|
+
* `delta.reasoning_content` (API reference, chat completion chunk); Groq
|
|
1769
|
+
* documents the parsed reasoning under a `reasoning` field. Neither is in
|
|
1770
|
+
* the OpenAI SDK's delta type, so the delta is read as a plain record.
|
|
1771
|
+
*/
|
|
1772
|
+
function readReasoningDelta(delta) {
|
|
1773
|
+
const record = delta;
|
|
1774
|
+
for (const key of ["reasoning_content", "reasoning"]) {
|
|
1775
|
+
const value = record[key];
|
|
1776
|
+
if (typeof value === "string" && value !== "") return value;
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
//#endregion
|
|
1781
|
+
//#region ../../packages/ai-openai/src/utils/hosted-output.ts
|
|
1782
|
+
/** Installed openai 7.23.0 ResponseFunctionWebSearch (action includes search/open_page/find).
|
|
1783
|
+
* https://developers.openai.com/api/docs/guides/tools-web-search
|
|
1784
|
+
* Count output items, never URLs, sources, answer text, or function calls.
|
|
1785
|
+
* Fees and search content tokens are separate; preview pricing varies:
|
|
1786
|
+
* https://developers.openai.com/api/docs/pricing#built-in-tools */
|
|
1787
|
+
function hostedOutput(usage, output, pricing) {
|
|
1788
|
+
const hosted = output.filter((item) => item.type !== "message" && item.type !== "reasoning" && item.type !== "function_call");
|
|
1789
|
+
const searches = hosted.filter((item) => item.type === "web_search_call");
|
|
1790
|
+
if (searches.length) usage.serverToolUse = {
|
|
1791
|
+
...usage.serverToolUse,
|
|
1792
|
+
webSearchRequests: searches.length
|
|
1793
|
+
};
|
|
1794
|
+
(0, _warlock_js_ai.attachCost)(usage, pricing);
|
|
1795
|
+
return hosted.length ? { openai: { output: hosted } } : void 0;
|
|
1796
|
+
}
|
|
1494
1797
|
|
|
1495
1798
|
//#endregion
|
|
1496
1799
|
//#region ../../packages/ai-openai/src/responses-model.ts
|
|
1497
1800
|
const LOG_MODULE$2 = "ai.openai";
|
|
1498
1801
|
/**
|
|
1802
|
+
* Responses output item types of built-in tools the CALLER must execute
|
|
1803
|
+
* (installed SDK `ResponseOutputItem` union). The adapter maps only
|
|
1804
|
+
* `function_call` to a neutral tool call, so these fail closed.
|
|
1805
|
+
*/
|
|
1806
|
+
const CLIENT_RUN_ITEM_TYPES = /* @__PURE__ */ new Set([
|
|
1807
|
+
"computer_call",
|
|
1808
|
+
"custom_tool_call",
|
|
1809
|
+
"local_shell_call",
|
|
1810
|
+
"shell_call",
|
|
1811
|
+
"apply_patch_call",
|
|
1812
|
+
"mcp_approval_request"
|
|
1813
|
+
]);
|
|
1814
|
+
/**
|
|
1499
1815
|
* OpenAI Responses API implementation of `ModelContract` (`api: "responses"`).
|
|
1500
1816
|
*
|
|
1501
1817
|
* **Role.** A sibling of `OpenAIModel` that speaks `POST /v1/responses`
|
|
@@ -1522,7 +1838,17 @@ const LOG_MODULE$2 = "ai.openai";
|
|
|
1522
1838
|
*
|
|
1523
1839
|
* **Streaming.** `stream()` uses `responses.create({ stream: true })` and yields
|
|
1524
1840
|
* the same chunk sequence as the Chat path: text `delta`s, then the
|
|
1525
|
-
* `tool-call`s, then one `done`.
|
|
1841
|
+
* `tool-call`s, then one `done`. With `streamChunks` it also yields
|
|
1842
|
+
* `reasoning` (reasoning summary / reasoning text deltas) and `tool-args`
|
|
1843
|
+
* (`response.function_call_arguments.delta`).
|
|
1844
|
+
*
|
|
1845
|
+
* **Provider-defined tools.** `ai.providerTool({ provider: "openai" })`
|
|
1846
|
+
* definitions are appended to `tools` verbatim. Hosted tools run inside the
|
|
1847
|
+
* response; their output items (`web_search_call`, …) are retained in
|
|
1848
|
+
* `providerMetadata.openai.output` for inspection and are not replayed
|
|
1849
|
+
* (with `store: false` the next turn only needs the answer text), and a
|
|
1850
|
+
* reasoning item that led into one is not replayed either (see
|
|
1851
|
+
* `pickTurnReasoning`).
|
|
1526
1852
|
*/
|
|
1527
1853
|
var OpenAIResponsesModel = class {
|
|
1528
1854
|
constructor(client, config, provider = "openai") {
|
|
@@ -1574,6 +1900,7 @@ var OpenAIResponsesModel = class {
|
|
|
1574
1900
|
hasToolCalls: toolCalls !== void 0
|
|
1575
1901
|
});
|
|
1576
1902
|
const usage = this.extractUsage(response.usage);
|
|
1903
|
+
const providerMetadata = hostedOutput(usage, output, this.pricing);
|
|
1577
1904
|
this.logger.debug(LOG_MODULE$2, "response", "call to responses.create succeeded", {
|
|
1578
1905
|
finishReason,
|
|
1579
1906
|
usage
|
|
@@ -1582,9 +1909,30 @@ var OpenAIResponsesModel = class {
|
|
|
1582
1909
|
content,
|
|
1583
1910
|
finishReason,
|
|
1584
1911
|
usage,
|
|
1912
|
+
...providerMetadata ? { providerMetadata } : {},
|
|
1585
1913
|
...toolCalls ? { toolCalls } : {}
|
|
1586
1914
|
};
|
|
1587
1915
|
}
|
|
1916
|
+
/** Count native Responses input tokens using the same converted input and tools. */
|
|
1917
|
+
async countTokens(messages, options) {
|
|
1918
|
+
const request = this.buildRequest(messages, options);
|
|
1919
|
+
const body = {
|
|
1920
|
+
model: request.model,
|
|
1921
|
+
input: request.input,
|
|
1922
|
+
instructions: request.instructions,
|
|
1923
|
+
tools: request.tools,
|
|
1924
|
+
tool_choice: request.tool_choice,
|
|
1925
|
+
reasoning: request.reasoning,
|
|
1926
|
+
text: request.text,
|
|
1927
|
+
parallel_tool_calls: request.parallel_tool_calls,
|
|
1928
|
+
truncation: request.truncation ?? void 0
|
|
1929
|
+
};
|
|
1930
|
+
try {
|
|
1931
|
+
return (await this.client.responses.inputTokens.count(body, options?.signal ? { signal: options.signal } : void 0)).input_tokens;
|
|
1932
|
+
} catch (thrown) {
|
|
1933
|
+
throw this.logAndWrap(thrown);
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1588
1936
|
/**
|
|
1589
1937
|
* Incremental streaming completion via `responses.create({ stream: true })`
|
|
1590
1938
|
* (the SDK's `Stream<ResponseStreamEvent>`, `responses.d.ts:55`). Yields the
|
|
@@ -1619,7 +1967,9 @@ var OpenAIResponsesModel = class {
|
|
|
1619
1967
|
throw this.logAndWrap(thrown);
|
|
1620
1968
|
}
|
|
1621
1969
|
const toolCalls = [];
|
|
1622
|
-
const
|
|
1970
|
+
const outputItems = [];
|
|
1971
|
+
const want = (0, _warlock_js_ai.resolveStreamChunks)(options);
|
|
1972
|
+
const streamingCalls = /* @__PURE__ */ new Map();
|
|
1623
1973
|
let terminal;
|
|
1624
1974
|
try {
|
|
1625
1975
|
for await (const event of events) {
|
|
@@ -1630,10 +1980,36 @@ var OpenAIResponsesModel = class {
|
|
|
1630
1980
|
};
|
|
1631
1981
|
continue;
|
|
1632
1982
|
}
|
|
1983
|
+
if (event.type === "response.reasoning_summary_text.delta" || event.type === "response.reasoning_text.delta") {
|
|
1984
|
+
if (want.reasoning && event.delta) yield {
|
|
1985
|
+
type: "reasoning",
|
|
1986
|
+
delta: event.delta
|
|
1987
|
+
};
|
|
1988
|
+
continue;
|
|
1989
|
+
}
|
|
1990
|
+
if (event.type === "response.output_item.added") {
|
|
1991
|
+
const { item } = event;
|
|
1992
|
+
if (item.type === "function_call" && item.id) streamingCalls.set(item.id, {
|
|
1993
|
+
callId: item.call_id,
|
|
1994
|
+
name: item.name
|
|
1995
|
+
});
|
|
1996
|
+
continue;
|
|
1997
|
+
}
|
|
1998
|
+
if (event.type === "response.function_call_arguments.delta") {
|
|
1999
|
+
const call = streamingCalls.get(event.item_id);
|
|
2000
|
+
if (want.toolArgs && call && event.delta) yield {
|
|
2001
|
+
type: "tool-args",
|
|
2002
|
+
callId: call.callId,
|
|
2003
|
+
name: call.name,
|
|
2004
|
+
delta: event.delta
|
|
2005
|
+
};
|
|
2006
|
+
continue;
|
|
2007
|
+
}
|
|
1633
2008
|
if (event.type === "response.output_item.done") {
|
|
1634
2009
|
const { item } = event;
|
|
1635
|
-
|
|
1636
|
-
|
|
2010
|
+
this.assertServerRunItem(item);
|
|
2011
|
+
outputItems.push(item);
|
|
2012
|
+
if (item.type === "function_call" && item.status !== "incomplete") toolCalls.push(this.toToolCall(item));
|
|
1637
2013
|
continue;
|
|
1638
2014
|
}
|
|
1639
2015
|
if (event.type === "response.completed" || event.type === "response.incomplete") {
|
|
@@ -1654,7 +2030,7 @@ var OpenAIResponsesModel = class {
|
|
|
1654
2030
|
} catch (thrown) {
|
|
1655
2031
|
throw this.logAndWrap(thrown);
|
|
1656
2032
|
}
|
|
1657
|
-
for (const call of attachReasoningReplay(toolCalls, this.name,
|
|
2033
|
+
for (const call of attachReasoningReplay(toolCalls, this.name, pickTurnReasoning(outputItems))) yield {
|
|
1658
2034
|
type: "tool-call",
|
|
1659
2035
|
id: call.id,
|
|
1660
2036
|
name: call.name,
|
|
@@ -1667,6 +2043,8 @@ var OpenAIResponsesModel = class {
|
|
|
1667
2043
|
hasToolCalls: toolCalls.length > 0
|
|
1668
2044
|
});
|
|
1669
2045
|
const usage = this.extractUsage(terminal.usage);
|
|
2046
|
+
const output = terminal.output?.length ? terminal.output : outputItems;
|
|
2047
|
+
const providerMetadata = hostedOutput(usage, output, this.pricing);
|
|
1670
2048
|
this.logger.debug(LOG_MODULE$2, "response", "Streaming call to responses.create succeeded", {
|
|
1671
2049
|
finishReason,
|
|
1672
2050
|
usage
|
|
@@ -1674,7 +2052,8 @@ var OpenAIResponsesModel = class {
|
|
|
1674
2052
|
yield {
|
|
1675
2053
|
type: "done",
|
|
1676
2054
|
finishReason,
|
|
1677
|
-
usage
|
|
2055
|
+
usage,
|
|
2056
|
+
...providerMetadata ? { providerMetadata } : {}
|
|
1678
2057
|
};
|
|
1679
2058
|
}
|
|
1680
2059
|
/**
|
|
@@ -1692,16 +2071,16 @@ var OpenAIResponsesModel = class {
|
|
|
1692
2071
|
*/
|
|
1693
2072
|
buildRequest(messages, options) {
|
|
1694
2073
|
const { instructions, input } = toResponsesInput(messages, this.name);
|
|
1695
|
-
const tools =
|
|
2074
|
+
const tools = this.buildTools(options);
|
|
1696
2075
|
const toolChoice = tools ? toResponsesToolChoice(options?.toolChoice) : void 0;
|
|
1697
2076
|
const maxTokens = options?.maxTokens ?? this.config.maxTokens;
|
|
1698
2077
|
const temperature = options?.temperature ?? this.config.temperature;
|
|
1699
|
-
return {
|
|
2078
|
+
return mergeProviderBody({
|
|
1700
2079
|
model: this.name,
|
|
1701
2080
|
...instructions !== void 0 ? { instructions } : {},
|
|
1702
2081
|
input,
|
|
1703
2082
|
store: false,
|
|
1704
|
-
include: ["reasoning.encrypted_content"],
|
|
2083
|
+
include: ["reasoning.encrypted_content", ...tools?.some((tool) => tool.type === "web_search" || tool.type === "web_search_2025_08_26" || tool.type === "web_search_preview" || tool.type === "web_search_preview_2025_03_11") ? ["web_search_call.action.sources"] : []],
|
|
1705
2084
|
...maxTokens !== void 0 ? { max_output_tokens: maxTokens } : {},
|
|
1706
2085
|
...!this.capabilities.reasoning && temperature !== void 0 ? { temperature } : {},
|
|
1707
2086
|
...tools ? { tools } : {},
|
|
@@ -1709,7 +2088,24 @@ var OpenAIResponsesModel = class {
|
|
|
1709
2088
|
...this.buildTextFormat(options?.responseSchema),
|
|
1710
2089
|
...this.buildReasoningParams(options?.reasoning),
|
|
1711
2090
|
...buildPromptCacheParams(this.provider, this.config)
|
|
1712
|
-
};
|
|
2091
|
+
}, options);
|
|
2092
|
+
}
|
|
2093
|
+
/**
|
|
2094
|
+
* The request `tools`: function tools (flat, `strict: false`), then the
|
|
2095
|
+
* `ai.providerTool({ provider: "openai" })` definitions verbatim — the
|
|
2096
|
+
* Responses built-in tools (`web_search`, `file_search`,
|
|
2097
|
+
* `code_interpreter`, `image_generation`, `mcp`, …). Other providers'
|
|
2098
|
+
* definitions are skipped with a warning. `undefined` when there are none.
|
|
2099
|
+
*/
|
|
2100
|
+
buildTools(options) {
|
|
2101
|
+
const { tools, definitions } = (0, _warlock_js_ai.selectProviderTools)(options, {
|
|
2102
|
+
accepts: [this.provider],
|
|
2103
|
+
module: LOG_MODULE$2,
|
|
2104
|
+
model: this.name,
|
|
2105
|
+
logger: this.logger
|
|
2106
|
+
});
|
|
2107
|
+
const mapped = [...toResponsesTools(tools) ?? [], ...definitions];
|
|
2108
|
+
return mapped.length > 0 ? mapped : void 0;
|
|
1713
2109
|
}
|
|
1714
2110
|
/**
|
|
1715
2111
|
* Structured output on Responses lives in `text.format`, and `name` /
|
|
@@ -1746,12 +2142,8 @@ var OpenAIResponsesModel = class {
|
|
|
1746
2142
|
readOutput(output) {
|
|
1747
2143
|
let content = "";
|
|
1748
2144
|
const toolCalls = [];
|
|
1749
|
-
const reasoningItems = [];
|
|
1750
2145
|
for (const item of output) {
|
|
1751
|
-
|
|
1752
|
-
reasoningItems.push(item);
|
|
1753
|
-
continue;
|
|
1754
|
-
}
|
|
2146
|
+
this.assertServerRunItem(item);
|
|
1755
2147
|
if (item.type === "message") {
|
|
1756
2148
|
for (const part of item.content) content += part.type === "refusal" ? part.refusal : part.text;
|
|
1757
2149
|
continue;
|
|
@@ -1760,9 +2152,24 @@ var OpenAIResponsesModel = class {
|
|
|
1760
2152
|
}
|
|
1761
2153
|
return {
|
|
1762
2154
|
content,
|
|
1763
|
-
toolCalls: toolCalls.length > 0 ? attachReasoningReplay(toolCalls, this.name,
|
|
2155
|
+
toolCalls: toolCalls.length > 0 ? attachReasoningReplay(toolCalls, this.name, pickTurnReasoning(output)) : void 0
|
|
1764
2156
|
};
|
|
1765
2157
|
}
|
|
2158
|
+
/**
|
|
2159
|
+
* Fail closed on a built-in tool call that YOUR code would have to run
|
|
2160
|
+
* (`shell_call`, `apply_patch_call`, `computer_call`, …, from a
|
|
2161
|
+
* provider-defined client tool). These items have their own output item
|
|
2162
|
+
* types that this adapter does not map, so the turn would otherwise end
|
|
2163
|
+
* silently with the call dropped. Hosted (server-run) tool items pass.
|
|
2164
|
+
*/
|
|
2165
|
+
assertServerRunItem(item) {
|
|
2166
|
+
if (!CLIENT_RUN_ITEM_TYPES.has(item.type)) return;
|
|
2167
|
+
throw new _warlock_js_ai.InvalidRequestError(`OpenAI returned a "${item.type}" item: that built-in tool is executed by the caller, which the Responses adapter does not support. Use OpenAI's server-run built-in tools (web_search, file_search, code_interpreter, image_generation, mcp without approval) with ai.providerTool(), or a function tool from ai.tool().`, { context: {
|
|
2168
|
+
provider: this.provider,
|
|
2169
|
+
model: this.name,
|
|
2170
|
+
itemType: item.type
|
|
2171
|
+
} });
|
|
2172
|
+
}
|
|
1766
2173
|
/** One completed `function_call` item as a neutral tool call (`call_id` -> `id`). */
|
|
1767
2174
|
toToolCall(item) {
|
|
1768
2175
|
return {
|
|
@@ -1791,7 +2198,7 @@ var OpenAIResponsesModel = class {
|
|
|
1791
2198
|
cachedTokens: raw.input_tokens_details?.cached_tokens,
|
|
1792
2199
|
cacheWriteTokens: raw.input_tokens_details?.cache_write_tokens,
|
|
1793
2200
|
reasoningTokens: raw.output_tokens_details?.reasoning_tokens
|
|
1794
|
-
});
|
|
2201
|
+
}, this.pricing);
|
|
1795
2202
|
}
|
|
1796
2203
|
/** Wrap (idempotently), log and return a failure for the caller to throw. */
|
|
1797
2204
|
logAndWrap(thrown) {
|
|
@@ -2014,8 +2421,9 @@ var OpenAISDK = class {
|
|
|
2014
2421
|
* coming from the same upstream.
|
|
2015
2422
|
*
|
|
2016
2423
|
* Pricing resolution: per-model `config.pricing` wins; otherwise the
|
|
2017
|
-
* SDK-level registry entry keyed by `config.name`; otherwise
|
|
2018
|
-
* `
|
|
2424
|
+
* SDK-level registry entry keyed by `config.name`; otherwise, when the
|
|
2425
|
+
* provider label is `"openai"`, the built-in list price
|
|
2426
|
+
* (`OPENAI_MODEL_PRICING`); otherwise `undefined` (no cost computed).
|
|
2019
2427
|
*
|
|
2020
2428
|
* `config.api` selects the wire API. Omitted or `"chat"` builds the Chat
|
|
2021
2429
|
* Completions `OpenAIModel`. `"responses"` builds `OpenAIResponsesModel`
|
|
@@ -2036,7 +2444,7 @@ var OpenAISDK = class {
|
|
|
2036
2444
|
model: config.name,
|
|
2037
2445
|
api
|
|
2038
2446
|
} });
|
|
2039
|
-
const resolvedPricing = config.pricing ?? this.pricing?.[config.name];
|
|
2447
|
+
const resolvedPricing = config.pricing ?? this.pricing?.[config.name] ?? (this.provider === "openai" ? openAIPricingFor(config.name) : void 0);
|
|
2040
2448
|
const resolvedConfig = resolvedPricing === config.pricing ? config : {
|
|
2041
2449
|
...config,
|
|
2042
2450
|
pricing: resolvedPricing
|
|
@@ -2123,9 +2531,265 @@ var OpenAISDK = class {
|
|
|
2123
2531
|
};
|
|
2124
2532
|
|
|
2125
2533
|
//#endregion
|
|
2534
|
+
//#region ../../packages/ai-openai/src/kimi-tools.ts
|
|
2535
|
+
/** USD per billable REST request, verified 2026-10-18:
|
|
2536
|
+
* https://platform.kimi.ai/docs/pricing/websearch
|
|
2537
|
+
* HTTP 200 + nonempty search_results / nonblank markdown only. Taxes excluded.
|
|
2538
|
+
* Namespaced rates avoid charging the generic counters a second time. */
|
|
2539
|
+
const KIMI_SERVER_TOOL_PRICING = Object.freeze({
|
|
2540
|
+
kimiSearchRequests: .002,
|
|
2541
|
+
kimiSearchProRequests: .003,
|
|
2542
|
+
kimiFetchRequests: .002
|
|
2543
|
+
});
|
|
2544
|
+
/** Standalone first-party REST search, registered as an ordinary client function tool.
|
|
2545
|
+
* Wire contract: https://platform.kimi.ai/docs/api/tools-search and tools-search-pro.
|
|
2546
|
+
* Reuse the API key / baseURL configuration passed to OpenAISDK; the default URL
|
|
2547
|
+
* here is Moonshot, independent of the model used by the agent. */
|
|
2548
|
+
function kimiSearchTool(options) {
|
|
2549
|
+
const mode = options.mode ?? "basic";
|
|
2550
|
+
if (mode !== "basic" && mode !== "pro") throw new _warlock_js_ai.InvalidRequestError("Invalid Kimi search mode.");
|
|
2551
|
+
const limit = integer(options.limit ?? 5, 1, 20, "limit");
|
|
2552
|
+
const timeout = integer(options.timeoutSeconds ?? 30, 1, 60, "timeoutSeconds");
|
|
2553
|
+
if (mode === "basic" && (options.sites !== void 0 || options.timeWindow !== void 0)) throw new _warlock_js_ai.InvalidRequestError("Kimi sites and timeWindow require mode: pro.");
|
|
2554
|
+
if (mode === "pro" && options.includeContent !== void 0) throw new _warlock_js_ai.InvalidRequestError("Kimi includeContent is available only in basic mode.");
|
|
2555
|
+
if (options.sites && (options.sites.length > 5 || options.sites.some((site) => !site || /[\s()]/u.test(site)))) throw new _warlock_js_ai.InvalidRequestError("Kimi sites accepts at most five nonempty sites without whitespace or parentheses.");
|
|
2556
|
+
const maxChars = contentLimit(options);
|
|
2557
|
+
const client = createClient(options);
|
|
2558
|
+
const counter = mode === "pro" ? "kimiSearchProRequests" : "kimiSearchRequests";
|
|
2559
|
+
return meteredTool({
|
|
2560
|
+
name: mode === "pro" ? "kimi_search_pro" : "kimi_search",
|
|
2561
|
+
description: "Search the web with Kimi and return cited sources and relevant text.",
|
|
2562
|
+
input: stringInput("query"),
|
|
2563
|
+
async execute({ query }, ctx, record) {
|
|
2564
|
+
const body = {
|
|
2565
|
+
text_query: query,
|
|
2566
|
+
limit,
|
|
2567
|
+
timeout_seconds: timeout,
|
|
2568
|
+
...mode === "basic" ? { include_content: options.includeContent ?? false } : {
|
|
2569
|
+
...options.sites !== void 0 ? { sites: options.sites } : {},
|
|
2570
|
+
...options.timeWindow !== void 0 ? { time_window: options.timeWindow } : {}
|
|
2571
|
+
}
|
|
2572
|
+
};
|
|
2573
|
+
const raw = await request(client, mode === "pro" ? "/tools/search_pro" : "/tools/search", body, ctx);
|
|
2574
|
+
if (!isObject(raw.data) || !Array.isArray(raw.data.search_results)) throw new _warlock_js_ai.ProviderError("Kimi returned invalid search_results.");
|
|
2575
|
+
record(raw.status === 200 && raw.data.search_results.length > 0);
|
|
2576
|
+
const budget = textBudget(maxChars);
|
|
2577
|
+
return {
|
|
2578
|
+
results: raw.data.search_results.slice(0, limit).map((value) => {
|
|
2579
|
+
if (!isObject(value)) throw new _warlock_js_ai.ProviderError("Kimi returned an invalid search result.");
|
|
2580
|
+
const result = {
|
|
2581
|
+
title: stringValue(value.title),
|
|
2582
|
+
url: stringValue(value.url),
|
|
2583
|
+
snippet: budget.take(stringValue(value.snippet)),
|
|
2584
|
+
siteName: stringValue(value.site_name),
|
|
2585
|
+
date: stringValue(value.date),
|
|
2586
|
+
authority: stringValue(value.authority)
|
|
2587
|
+
};
|
|
2588
|
+
if (mode === "basic" && options.includeContent) result.text = budget.take(stringValue(value.text));
|
|
2589
|
+
if (mode === "pro") {
|
|
2590
|
+
if (!Array.isArray(value.chunks)) throw new _warlock_js_ai.ProviderError("Kimi returned invalid search chunks.");
|
|
2591
|
+
result.chunks = value.chunks.map((chunk) => {
|
|
2592
|
+
if (!isObject(chunk) || typeof chunk.score !== "number" || !Number.isFinite(chunk.score)) throw new _warlock_js_ai.ProviderError("Kimi returned an invalid search chunk.");
|
|
2593
|
+
return {
|
|
2594
|
+
text: budget.take(stringValue(chunk.text)),
|
|
2595
|
+
score: chunk.score
|
|
2596
|
+
};
|
|
2597
|
+
}).filter((chunk) => chunk.text.length > 0);
|
|
2598
|
+
}
|
|
2599
|
+
return result;
|
|
2600
|
+
}),
|
|
2601
|
+
truncated: budget.truncated || raw.data.search_results.length > limit
|
|
2602
|
+
};
|
|
2603
|
+
}
|
|
2604
|
+
}, counter, options.pricing);
|
|
2605
|
+
}
|
|
2606
|
+
/** First-party URL-to-Markdown REST tool.
|
|
2607
|
+
* https://platform.kimi.ai/docs/api/tools-fetch — only http/https inputs. */
|
|
2608
|
+
function kimiFetchTool(options) {
|
|
2609
|
+
const client = createClient(options);
|
|
2610
|
+
const maxChars = contentLimit(options);
|
|
2611
|
+
return meteredTool({
|
|
2612
|
+
name: "kimi_fetch",
|
|
2613
|
+
description: "Read a web page with Kimi and return its title, URL and Markdown body.",
|
|
2614
|
+
input: stringInput("url"),
|
|
2615
|
+
async execute({ url }, ctx, record) {
|
|
2616
|
+
let parsed;
|
|
2617
|
+
try {
|
|
2618
|
+
parsed = new URL(url);
|
|
2619
|
+
} catch {
|
|
2620
|
+
throw new _warlock_js_ai.InvalidRequestError("Kimi fetch requires an http/https URL.");
|
|
2621
|
+
}
|
|
2622
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new _warlock_js_ai.InvalidRequestError("Kimi fetch requires an http/https URL.");
|
|
2623
|
+
const raw = await request(client, "/tools/fetch", { url }, ctx);
|
|
2624
|
+
if (!isObject(raw.data) || typeof raw.data.markdown !== "string") throw new _warlock_js_ai.ProviderError("Kimi returned invalid fetch content.");
|
|
2625
|
+
record(raw.status === 200 && raw.data.markdown.trim().length > 0);
|
|
2626
|
+
return {
|
|
2627
|
+
url: stringValue(raw.data.url),
|
|
2628
|
+
title: stringValue(raw.data.title),
|
|
2629
|
+
markdown: raw.data.markdown.slice(0, maxChars),
|
|
2630
|
+
truncated: raw.data.markdown.length > maxChars
|
|
2631
|
+
};
|
|
2632
|
+
}
|
|
2633
|
+
}, "kimiFetchRequests", options.pricing);
|
|
2634
|
+
}
|
|
2635
|
+
function createClient(options) {
|
|
2636
|
+
const { apiKey, baseURL, fetch, timeout, defaultHeaders } = options;
|
|
2637
|
+
if (!apiKey) throw new _warlock_js_ai.ProviderAuthError("Kimi tools require an explicitly configured API key.");
|
|
2638
|
+
return new openai.default({
|
|
2639
|
+
apiKey,
|
|
2640
|
+
baseURL: baseURL ?? "https://api.moonshot.ai/v1",
|
|
2641
|
+
fetch,
|
|
2642
|
+
timeout,
|
|
2643
|
+
defaultHeaders,
|
|
2644
|
+
maxRetries: 0
|
|
2645
|
+
});
|
|
2646
|
+
}
|
|
2647
|
+
async function request(client, path, body, ctx) {
|
|
2648
|
+
try {
|
|
2649
|
+
ctx?.signal?.throwIfAborted();
|
|
2650
|
+
const response = await client.post(path, {
|
|
2651
|
+
body,
|
|
2652
|
+
signal: ctx?.signal
|
|
2653
|
+
}).withResponse();
|
|
2654
|
+
return {
|
|
2655
|
+
data: response.data,
|
|
2656
|
+
status: response.response.status
|
|
2657
|
+
};
|
|
2658
|
+
} catch (thrown) {
|
|
2659
|
+
if (ctx?.signal?.aborted) throw new _warlock_js_ai.AgentCancelledError("Kimi tool request cancelled.", { cause: thrown });
|
|
2660
|
+
if (thrown instanceof openai.APIError) {
|
|
2661
|
+
const context = {
|
|
2662
|
+
status: thrown.status,
|
|
2663
|
+
type: thrown.type,
|
|
2664
|
+
requestId: thrown.headers?.get("x-msh-track-id")
|
|
2665
|
+
};
|
|
2666
|
+
if (thrown.status === 408 || thrown.status === 504) throw new _warlock_js_ai.ProviderTimeoutError(thrown.message, {
|
|
2667
|
+
cause: thrown,
|
|
2668
|
+
context
|
|
2669
|
+
});
|
|
2670
|
+
if (thrown.status === 429) {
|
|
2671
|
+
const retry = Number(thrown.headers?.get("retry-after") ?? NaN);
|
|
2672
|
+
throw new _warlock_js_ai.ProviderRateLimitError(thrown.message, {
|
|
2673
|
+
cause: thrown,
|
|
2674
|
+
context,
|
|
2675
|
+
retryAfter: Number.isFinite(retry) && retry >= 0 ? retry * 1e3 : void 0
|
|
2676
|
+
});
|
|
2677
|
+
}
|
|
2678
|
+
if (thrown.status === 403 && !thrown.type) throw new _warlock_js_ai.ProviderAuthError(thrown.message, {
|
|
2679
|
+
cause: thrown,
|
|
2680
|
+
context
|
|
2681
|
+
});
|
|
2682
|
+
}
|
|
2683
|
+
throw wrapOpenAIError(thrown);
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
/** Each invoke owns its ledger, including concurrent calls and post-response failures.
|
|
2687
|
+
* ToolContract.invoke is the existing accounting boundary used by agents/reports. */
|
|
2688
|
+
function meteredTool(config, counter, prices = KIMI_SERVER_TOOL_PRICING) {
|
|
2689
|
+
const rate = prices[counter];
|
|
2690
|
+
if (rate !== void 0 && (!Number.isFinite(rate) || rate < 0)) throw new _warlock_js_ai.InvalidRequestError("Kimi per-call pricing must be finite and nonnegative.");
|
|
2691
|
+
return {
|
|
2692
|
+
...(0, _warlock_js_ai.tool)({
|
|
2693
|
+
...config,
|
|
2694
|
+
execute: (input, ctx) => config.execute(input, ctx, () => {})
|
|
2695
|
+
}),
|
|
2696
|
+
async invoke(input, ctx) {
|
|
2697
|
+
let usage = {
|
|
2698
|
+
input: 0,
|
|
2699
|
+
output: 0,
|
|
2700
|
+
total: 0
|
|
2701
|
+
};
|
|
2702
|
+
const result = await (0, _warlock_js_ai.tool)({
|
|
2703
|
+
...config,
|
|
2704
|
+
execute: (validated, context) => config.execute(validated, context, (billable) => {
|
|
2705
|
+
const count = billable ? 1 : 0;
|
|
2706
|
+
usage = {
|
|
2707
|
+
input: 0,
|
|
2708
|
+
output: 0,
|
|
2709
|
+
total: 0,
|
|
2710
|
+
serverToolUse: {
|
|
2711
|
+
[counter]: count,
|
|
2712
|
+
[counter === "kimiFetchRequests" ? "webFetchRequests" : "webSearchRequests"]: count
|
|
2713
|
+
}
|
|
2714
|
+
};
|
|
2715
|
+
if (rate !== void 0) usage.cost = (0, _warlock_js_ai.computeCost)(usage, {
|
|
2716
|
+
input: 0,
|
|
2717
|
+
output: 0,
|
|
2718
|
+
serverTools: { [counter]: rate }
|
|
2719
|
+
});
|
|
2720
|
+
})
|
|
2721
|
+
}).invoke(input, ctx);
|
|
2722
|
+
return {
|
|
2723
|
+
...result,
|
|
2724
|
+
usage,
|
|
2725
|
+
report: {
|
|
2726
|
+
...result.report,
|
|
2727
|
+
usage
|
|
2728
|
+
}
|
|
2729
|
+
};
|
|
2730
|
+
}
|
|
2731
|
+
};
|
|
2732
|
+
}
|
|
2733
|
+
function integer(value, min, max, name) {
|
|
2734
|
+
if (!Number.isInteger(value) || value < min || value > max) throw new _warlock_js_ai.InvalidRequestError(`Kimi ${name} must be an integer from ${min} to ${max}.`);
|
|
2735
|
+
return value;
|
|
2736
|
+
}
|
|
2737
|
+
function contentLimit(options) {
|
|
2738
|
+
return integer(options.maxContentChars ?? 12e3, 1, Number.MAX_SAFE_INTEGER, "maxContentChars");
|
|
2739
|
+
}
|
|
2740
|
+
function isObject(value) {
|
|
2741
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2742
|
+
}
|
|
2743
|
+
function stringValue(value) {
|
|
2744
|
+
if (typeof value !== "string") throw new _warlock_js_ai.ProviderError("Kimi returned an invalid text field.");
|
|
2745
|
+
return value;
|
|
2746
|
+
}
|
|
2747
|
+
function textBudget(max) {
|
|
2748
|
+
return {
|
|
2749
|
+
truncated: false,
|
|
2750
|
+
take(text) {
|
|
2751
|
+
const result = text.slice(0, max);
|
|
2752
|
+
if (text.length > max) this.truncated = true;
|
|
2753
|
+
max -= result.length;
|
|
2754
|
+
return result;
|
|
2755
|
+
}
|
|
2756
|
+
};
|
|
2757
|
+
}
|
|
2758
|
+
function stringInput(key) {
|
|
2759
|
+
return {
|
|
2760
|
+
jsonSchema: {
|
|
2761
|
+
type: "object",
|
|
2762
|
+
properties: { [key]: {
|
|
2763
|
+
type: "string",
|
|
2764
|
+
minLength: 1
|
|
2765
|
+
} },
|
|
2766
|
+
required: [key],
|
|
2767
|
+
additionalProperties: false
|
|
2768
|
+
},
|
|
2769
|
+
"~standard": {
|
|
2770
|
+
version: 1,
|
|
2771
|
+
vendor: "kimi",
|
|
2772
|
+
validate(value) {
|
|
2773
|
+
if (!isObject(value) || typeof value[key] !== "string" || !value[key].trim()) return { issues: [{
|
|
2774
|
+
message: `${key} must be a nonempty string`,
|
|
2775
|
+
path: [key]
|
|
2776
|
+
}] };
|
|
2777
|
+
return { value: { [key]: value[key] } };
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
};
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2783
|
+
//#endregion
|
|
2784
|
+
exports.KIMI_SERVER_TOOL_PRICING = KIMI_SERVER_TOOL_PRICING;
|
|
2785
|
+
exports.OPENAI_MODEL_PRICING = OPENAI_MODEL_PRICING;
|
|
2786
|
+
exports.OPENAI_SERVER_TOOL_PRICING = OPENAI_SERVER_TOOL_PRICING;
|
|
2126
2787
|
exports.OpenAIEmbedder = OpenAIEmbedder;
|
|
2127
2788
|
exports.OpenAIImageModel = OpenAIImageModel;
|
|
2128
2789
|
exports.OpenAISDK = OpenAISDK;
|
|
2129
2790
|
exports.OpenAISpeechModel = OpenAISpeechModel;
|
|
2130
2791
|
exports.OpenAITranscriptionModel = OpenAITranscriptionModel;
|
|
2792
|
+
exports.kimiFetchTool = kimiFetchTool;
|
|
2793
|
+
exports.kimiSearchTool = kimiSearchTool;
|
|
2794
|
+
exports.openAIPricingFor = openAIPricingFor;
|
|
2131
2795
|
//# sourceMappingURL=index.cjs.map
|