@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.
Files changed (58) hide show
  1. package/CHANGELOG.md +42 -1
  2. package/README.md +3 -3
  3. package/cjs/index.cjs +715 -51
  4. package/cjs/index.cjs.map +1 -1
  5. package/esm/config.type.d.mts +13 -13
  6. package/esm/config.type.d.mts.map +1 -1
  7. package/esm/embedder.d.mts +1 -3
  8. package/esm/embedder.d.mts.map +1 -1
  9. package/esm/embedder.mjs.map +1 -1
  10. package/esm/image.d.mts +1 -3
  11. package/esm/image.d.mts.map +1 -1
  12. package/esm/index.d.mts +4 -1
  13. package/esm/index.mjs +3 -1
  14. package/esm/kimi-tools.d.mts +69 -0
  15. package/esm/kimi-tools.d.mts.map +1 -0
  16. package/esm/kimi-tools.mjs +256 -0
  17. package/esm/kimi-tools.mjs.map +1 -0
  18. package/esm/known-pricing.d.mts +39 -0
  19. package/esm/known-pricing.d.mts.map +1 -0
  20. package/esm/known-pricing.mjs +121 -0
  21. package/esm/known-pricing.mjs.map +1 -0
  22. package/esm/model.mjs +82 -23
  23. package/esm/model.mjs.map +1 -1
  24. package/esm/provider-options.d.mts +13 -0
  25. package/esm/provider-options.d.mts.map +1 -0
  26. package/esm/provider-options.mjs +56 -0
  27. package/esm/provider-options.mjs.map +1 -0
  28. package/esm/responses-model.mjs +125 -19
  29. package/esm/responses-model.mjs.map +1 -1
  30. package/esm/sdk.d.mts +4 -5
  31. package/esm/sdk.d.mts.map +1 -1
  32. package/esm/sdk.mjs +5 -3
  33. package/esm/sdk.mjs.map +1 -1
  34. package/esm/speech.d.mts +1 -3
  35. package/esm/speech.d.mts.map +1 -1
  36. package/esm/transcription.d.mts +1 -3
  37. package/esm/transcription.d.mts.map +1 -1
  38. package/esm/transcription.mjs.map +1 -1
  39. package/esm/utils/build-usage.mjs +14 -2
  40. package/esm/utils/build-usage.mjs.map +1 -1
  41. package/esm/utils/hosted-output.mjs +22 -0
  42. package/esm/utils/hosted-output.mjs.map +1 -0
  43. package/esm/utils/index.mjs +2 -2
  44. package/esm/utils/map-responses-status.mjs.map +1 -1
  45. package/esm/utils/prompt-cache-params.mjs +25 -6
  46. package/esm/utils/prompt-cache-params.mjs.map +1 -1
  47. package/esm/utils/reasoning-replay.mjs +23 -1
  48. package/esm/utils/reasoning-replay.mjs.map +1 -1
  49. package/esm/utils/structured-output.mjs +1 -1
  50. package/esm/utils/structured-output.mjs.map +1 -1
  51. package/esm/utils/to-openai-messages.mjs.map +1 -1
  52. package/esm/utils/to-openai-tools.mjs.map +1 -1
  53. package/esm/utils/wrap-openai-error.mjs +6 -1
  54. package/esm/utils/wrap-openai-error.mjs.map +1 -1
  55. package/llms-full.txt +235 -37
  56. package/llms.txt +1 -1
  57. package/package.json +3 -3
  58. 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
- return {
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
- * Build the OpenAI-only prompt-cache request fields from the model config.
275
- * Returns nothing for any provider label other than `"openai"`: wrapper
276
- * endpoints use the same transport but may reject these OpenAI-only fields.
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
- tools: toOpenAITools(options?.tools),
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
- tools: toOpenAITools(options?.tools),
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
- const usage = {
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 reasoningItems = [];
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
- if (item.type === "reasoning") reasoningItems.push(item);
1636
- else if (item.type === "function_call" && item.status !== "incomplete") toolCalls.push(this.toToolCall(item));
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, reasoningItems)) yield {
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 = toResponsesTools(options?.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
- if (item.type === "reasoning") {
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, reasoningItems) : void 0
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
- * `undefined` (no cost computed).
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