@warlock.js/ai-openai 5.26.0 → 5.28.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 (38) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/cjs/index.cjs +958 -280
  3. package/cjs/index.cjs.map +1 -1
  4. package/esm/config.type.d.mts +22 -0
  5. package/esm/config.type.d.mts.map +1 -1
  6. package/esm/model.mjs +26 -119
  7. package/esm/model.mjs.map +1 -1
  8. package/esm/responses-model.mjs +329 -0
  9. package/esm/responses-model.mjs.map +1 -0
  10. package/esm/sdk.d.mts +7 -0
  11. package/esm/sdk.d.mts.map +1 -1
  12. package/esm/sdk.mjs +21 -1
  13. package/esm/sdk.mjs.map +1 -1
  14. package/esm/utils/build-usage.mjs +23 -0
  15. package/esm/utils/build-usage.mjs.map +1 -0
  16. package/esm/utils/index.mjs +9 -1
  17. package/esm/utils/map-responses-status.mjs +54 -0
  18. package/esm/utils/map-responses-status.mjs.map +1 -0
  19. package/esm/utils/prompt-cache-params.mjs +17 -0
  20. package/esm/utils/prompt-cache-params.mjs.map +1 -0
  21. package/esm/utils/reasoning-effort.mjs +35 -0
  22. package/esm/utils/reasoning-effort.mjs.map +1 -0
  23. package/esm/utils/reasoning-replay.mjs +85 -0
  24. package/esm/utils/reasoning-replay.mjs.map +1 -0
  25. package/esm/utils/structured-output.mjs +84 -0
  26. package/esm/utils/structured-output.mjs.map +1 -0
  27. package/esm/utils/to-openai-messages.mjs +9 -2
  28. package/esm/utils/to-openai-messages.mjs.map +1 -1
  29. package/esm/utils/to-openai-tools.mjs +3 -3
  30. package/esm/utils/to-openai-tools.mjs.map +1 -1
  31. package/esm/utils/to-responses-input.mjs +112 -0
  32. package/esm/utils/to-responses-input.mjs.map +1 -0
  33. package/esm/utils/to-responses-tools.mjs +51 -0
  34. package/esm/utils/to-responses-tools.mjs.map +1 -0
  35. package/llms-full.txt +20 -3
  36. package/llms.txt +1 -1
  37. package/package.json +3 -3
  38. package/skills/setup-openai/SKILL.md +20 -3
@@ -0,0 +1,35 @@
1
+ //#region ../ai-openai/src/utils/reasoning-effort.ts
2
+ /**
3
+ * The installed OpenAI SDK 7.23.0 declares `reasoning_effort` as
4
+ * `none | minimal | low | medium | high | xhigh | max` in
5
+ * `openai/src/resources/shared.ts:367`. Its type notes that model support
6
+ * varies but does not provide a model-family-specific accepted set, so map by
7
+ * that union alone. This exhaustive record makes a future core level a
8
+ * typecheck failure until its OpenAI clamp is consciously selected.
9
+ *
10
+ * Both the Chat Completions adapter (`reasoning_effort`) and the Responses
11
+ * adapter (`reasoning.effort`) read this one table, so the two can never
12
+ * disagree on how a neutral level reaches the wire.
13
+ */
14
+ const EFFORT_TO_OPENAI_EFFORT = {
15
+ none: "none",
16
+ minimal: "minimal",
17
+ low: "low",
18
+ medium: "medium",
19
+ high: "high",
20
+ xhigh: "xhigh",
21
+ max: "max"
22
+ };
23
+ /**
24
+ * Map a neutral `ReasoningEffort` to the value OpenAI accepts.
25
+ *
26
+ * @example
27
+ * toOpenAIReasoningEffort("high"); // "high"
28
+ */
29
+ function toOpenAIReasoningEffort(effort) {
30
+ return EFFORT_TO_OPENAI_EFFORT[effort];
31
+ }
32
+
33
+ //#endregion
34
+ export { toOpenAIReasoningEffort };
35
+ //# sourceMappingURL=reasoning-effort.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reasoning-effort.mjs","names":[],"sources":["../../../../../../../ai-openai/src/utils/reasoning-effort.ts"],"sourcesContent":["import type { ReasoningEffort } from \"@warlock.js/ai\";\nimport type OpenAI from \"openai\";\n\n/** The effort strings the OpenAI wire accepts (shared by Chat Completions and Responses). */\nexport type OpenAIReasoningEffort = NonNullable<\n OpenAI.Chat.Completions.ChatCompletionCreateParams[\"reasoning_effort\"]\n>;\n\n/**\n * The installed OpenAI SDK 7.23.0 declares `reasoning_effort` as\n * `none | minimal | low | medium | high | xhigh | max` in\n * `openai/src/resources/shared.ts:367`. Its type notes that model support\n * varies but does not provide a model-family-specific accepted set, so map by\n * that union alone. This exhaustive record makes a future core level a\n * typecheck failure until its OpenAI clamp is consciously selected.\n *\n * Both the Chat Completions adapter (`reasoning_effort`) and the Responses\n * adapter (`reasoning.effort`) read this one table, so the two can never\n * disagree on how a neutral level reaches the wire.\n */\nconst EFFORT_TO_OPENAI_EFFORT: Record<ReasoningEffort, OpenAIReasoningEffort> = {\n none: \"none\",\n minimal: \"minimal\",\n low: \"low\",\n medium: \"medium\",\n high: \"high\",\n xhigh: \"xhigh\",\n max: \"max\",\n};\n\n/**\n * Map a neutral `ReasoningEffort` to the value OpenAI accepts.\n *\n * @example\n * toOpenAIReasoningEffort(\"high\"); // \"high\"\n */\nexport function toOpenAIReasoningEffort(effort: ReasoningEffort): OpenAIReasoningEffort {\n return EFFORT_TO_OPENAI_EFFORT[effort];\n}\n"],"mappings":";;;;;;;;;;;;;AAoBA,MAAM,0BAA0E;CAC9E,MAAM;CACN,SAAS;CACT,KAAK;CACL,QAAQ;CACR,MAAM;CACN,OAAO;CACP,KAAK;AACP;;;;;;;AAQA,SAAgB,wBAAwB,QAAgD;CACtF,OAAO,wBAAwB;AACjC"}
@@ -0,0 +1,85 @@
1
+ //#region ../ai-openai/src/utils/reasoning-replay.ts
2
+ /**
3
+ * Key under `ModelToolCallRequest.providerMetadata` owned by the Responses
4
+ * adapter. This name and the shape below are PERSISTED in users' conversation
5
+ * memory, so they are part of the public contract: do not rename.
6
+ */
7
+ const RESPONSES_METADATA_KEY = "openaiResponses";
8
+ /**
9
+ * Keep only the reasoning items that can actually be replayed. With
10
+ * `store: false` the server keeps nothing, so an item without
11
+ * `encrypted_content` has nothing to restore and replaying its bare `id`
12
+ * would be rejected as an unknown item.
13
+ */
14
+ function pickReplayableReasoning(items) {
15
+ return items.filter((item) => typeof item.encrypted_content === "string" && item.encrypted_content !== "");
16
+ }
17
+ /**
18
+ * Attach the turn's reasoning items to the FIRST tool call only (parallel
19
+ * calls do not each repeat the blobs). Returns the calls unchanged when
20
+ * there is nothing to replay or no tool call to ride on (a text-only turn is
21
+ * deliberately not replayed, decision 2). Does not mutate its input.
22
+ */
23
+ function attachReasoningReplay(toolCalls, model, reasoningItems) {
24
+ const replayable = pickReplayableReasoning(reasoningItems);
25
+ const [first, ...rest] = toolCalls;
26
+ if (first === void 0 || replayable.length === 0) return [...toolCalls];
27
+ const metadata = {
28
+ model,
29
+ reasoningItems: replayable
30
+ };
31
+ return [{
32
+ ...first,
33
+ providerMetadata: {
34
+ ...first.providerMetadata,
35
+ [RESPONSES_METADATA_KEY]: metadata
36
+ }
37
+ }, ...rest];
38
+ }
39
+ /**
40
+ * Read the stored reasoning items off an assistant turn's tool calls, for
41
+ * replay on `model`. Returns `[]` when the key is absent or malformed, or
42
+ * when the stored `model` differs from `model` (encrypted blobs are not
43
+ * portable across models, design section 4). De-duplicates by item `id`.
44
+ */
45
+ function readReasoningReplay(toolCalls, model) {
46
+ const seen = /* @__PURE__ */ new Set();
47
+ const items = [];
48
+ for (const call of toolCalls) {
49
+ const stored = call.providerMetadata?.[RESPONSES_METADATA_KEY];
50
+ if (!isReplayMetadata(stored) || stored.model !== model) continue;
51
+ for (const item of stored.reasoningItems) {
52
+ if (item?.type !== "reasoning" || seen.has(item.id)) continue;
53
+ seen.add(item.id);
54
+ items.push(item);
55
+ }
56
+ }
57
+ return items;
58
+ }
59
+ /**
60
+ * Shape a stored reasoning item for the next request. Sends the four fields
61
+ * a stateless replay needs (`type`, `id`, `summary`, `encrypted_content`) and
62
+ * drops the response-only `status` and the optional raw `content`.
63
+ *
64
+ * UNVERIFIED: the SDK types accept the full item as input (`ResponseInputItem`
65
+ * includes `ResponseReasoningItem`) and do not say which fields the API
66
+ * refuses on replay with `store: false`. This is the conservative guess; a
67
+ * live probe settles it.
68
+ */
69
+ function toReplayReasoningItem(item) {
70
+ return {
71
+ type: "reasoning",
72
+ id: item.id,
73
+ summary: Array.isArray(item.summary) ? item.summary : [],
74
+ encrypted_content: item.encrypted_content
75
+ };
76
+ }
77
+ function isReplayMetadata(value) {
78
+ if (typeof value !== "object" || value === null) return false;
79
+ const candidate = value;
80
+ return typeof candidate.model === "string" && Array.isArray(candidate.reasoningItems);
81
+ }
82
+
83
+ //#endregion
84
+ export { RESPONSES_METADATA_KEY, attachReasoningReplay, pickReplayableReasoning, readReasoningReplay, toReplayReasoningItem };
85
+ //# sourceMappingURL=reasoning-replay.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reasoning-replay.mjs","names":[],"sources":["../../../../../../../ai-openai/src/utils/reasoning-replay.ts"],"sourcesContent":["import type { ModelToolCallRequest } from \"@warlock.js/ai\";\nimport type OpenAI from \"openai\";\n\n/** A `reasoning` output item as the Responses API returns it. */\nexport type ResponsesReasoningItem = OpenAI.Responses.ResponseReasoningItem;\n\n/**\n * Key under `ModelToolCallRequest.providerMetadata` owned by the Responses\n * adapter. This name and the shape below are PERSISTED in users' conversation\n * memory, so they are part of the public contract: do not rename.\n */\nexport const RESPONSES_METADATA_KEY = \"openaiResponses\";\n\n/**\n * What the Responses adapter stores on the FIRST tool call of an assistant\n * turn so the next request can replay the model's reasoning (design\n * section 4, decisions 2 and 3).\n */\nexport type ResponsesReplayMetadata = {\n /** Model the items were produced by; replay is skipped when it differs. */\n model: string;\n /** Reasoning items of the turn, in the order the API emitted them. */\n reasoningItems: ResponsesReasoningItem[];\n};\n\n/**\n * Keep only the reasoning items that can actually be replayed. With\n * `store: false` the server keeps nothing, so an item without\n * `encrypted_content` has nothing to restore and replaying its bare `id`\n * would be rejected as an unknown item.\n */\nexport function pickReplayableReasoning(\n items: readonly ResponsesReasoningItem[],\n): ResponsesReasoningItem[] {\n return items.filter(\n (item) => typeof item.encrypted_content === \"string\" && item.encrypted_content !== \"\",\n );\n}\n\n/**\n * Attach the turn's reasoning items to the FIRST tool call only (parallel\n * calls do not each repeat the blobs). Returns the calls unchanged when\n * there is nothing to replay or no tool call to ride on (a text-only turn is\n * deliberately not replayed, decision 2). Does not mutate its input.\n */\nexport function attachReasoningReplay(\n toolCalls: readonly ModelToolCallRequest[],\n model: string,\n reasoningItems: readonly ResponsesReasoningItem[],\n): ModelToolCallRequest[] {\n const replayable = pickReplayableReasoning(reasoningItems);\n const [first, ...rest] = toolCalls;\n\n if (first === undefined || replayable.length === 0) {\n return [...toolCalls];\n }\n\n const metadata: ResponsesReplayMetadata = { model, reasoningItems: replayable };\n\n return [\n { ...first, providerMetadata: { ...first.providerMetadata, [RESPONSES_METADATA_KEY]: metadata } },\n ...rest,\n ];\n}\n\n/**\n * Read the stored reasoning items off an assistant turn's tool calls, for\n * replay on `model`. Returns `[]` when the key is absent or malformed, or\n * when the stored `model` differs from `model` (encrypted blobs are not\n * portable across models, design section 4). De-duplicates by item `id`.\n */\nexport function readReasoningReplay(\n toolCalls: readonly ModelToolCallRequest[],\n model: string,\n): ResponsesReasoningItem[] {\n const seen = new Set<string>();\n const items: ResponsesReasoningItem[] = [];\n\n for (const call of toolCalls) {\n const stored = call.providerMetadata?.[RESPONSES_METADATA_KEY];\n\n if (!isReplayMetadata(stored) || stored.model !== model) {\n continue;\n }\n\n for (const item of stored.reasoningItems) {\n if (item?.type !== \"reasoning\" || seen.has(item.id)) {\n continue;\n }\n\n seen.add(item.id);\n items.push(item);\n }\n }\n\n return items;\n}\n\n/**\n * Shape a stored reasoning item for the next request. Sends the four fields\n * a stateless replay needs (`type`, `id`, `summary`, `encrypted_content`) and\n * drops the response-only `status` and the optional raw `content`.\n *\n * UNVERIFIED: the SDK types accept the full item as input (`ResponseInputItem`\n * includes `ResponseReasoningItem`) and do not say which fields the API\n * refuses on replay with `store: false`. This is the conservative guess; a\n * live probe settles it.\n */\nexport function toReplayReasoningItem(item: ResponsesReasoningItem): ResponsesReasoningItem {\n return {\n type: \"reasoning\",\n id: item.id,\n summary: Array.isArray(item.summary) ? item.summary : [],\n encrypted_content: item.encrypted_content,\n };\n}\n\nfunction isReplayMetadata(value: unknown): value is ResponsesReplayMetadata {\n if (typeof value !== \"object\" || value === null) {\n return false;\n }\n\n const candidate = value as Partial<ResponsesReplayMetadata>;\n\n return typeof candidate.model === \"string\" && Array.isArray(candidate.reasoningItems);\n}\n"],"mappings":";;;;;;AAWA,MAAa,yBAAyB;;;;;;;AAoBtC,SAAgB,wBACd,OAC0B;CAC1B,OAAO,MAAM,QACV,SAAS,OAAO,KAAK,sBAAsB,YAAY,KAAK,sBAAsB,EACrF;AACF;;;;;;;AAQA,SAAgB,sBACd,WACA,OACA,gBACwB;CACxB,MAAM,aAAa,wBAAwB,cAAc;CACzD,MAAM,CAAC,OAAO,GAAG,QAAQ;CAEzB,IAAI,UAAU,UAAa,WAAW,WAAW,GAC/C,OAAO,CAAC,GAAG,SAAS;CAGtB,MAAM,WAAoC;EAAE;EAAO,gBAAgB;CAAW;CAE9E,OAAO,CACL;EAAE,GAAG;EAAO,kBAAkB;GAAE,GAAG,MAAM;IAAmB,yBAAyB;EAAS;CAAE,GAChG,GAAG,IACL;AACF;;;;;;;AAQA,SAAgB,oBACd,WACA,OAC0B;CAC1B,MAAM,uBAAO,IAAI,IAAY;CAC7B,MAAM,QAAkC,CAAC;CAEzC,KAAK,MAAM,QAAQ,WAAW;EAC5B,MAAM,SAAS,KAAK,mBAAmB;EAEvC,IAAI,CAAC,iBAAiB,MAAM,KAAK,OAAO,UAAU,OAChD;EAGF,KAAK,MAAM,QAAQ,OAAO,gBAAgB;GACxC,IAAI,MAAM,SAAS,eAAe,KAAK,IAAI,KAAK,EAAE,GAChD;GAGF,KAAK,IAAI,KAAK,EAAE;GAChB,MAAM,KAAK,IAAI;EACjB;CACF;CAEA,OAAO;AACT;;;;;;;;;;;AAYA,SAAgB,sBAAsB,MAAsD;CAC1F,OAAO;EACL,MAAM;EACN,IAAI,KAAK;EACT,SAAS,MAAM,QAAQ,KAAK,OAAO,IAAI,KAAK,UAAU,CAAC;EACvD,mBAAmB,KAAK;CAC1B;AACF;AAEA,SAAS,iBAAiB,OAAkD;CAC1E,IAAI,OAAO,UAAU,YAAY,UAAU,MACzC,OAAO;CAGT,MAAM,YAAY;CAElB,OAAO,OAAO,UAAU,UAAU,YAAY,MAAM,QAAQ,UAAU,cAAc;AACtF"}
@@ -0,0 +1,84 @@
1
+ //#region ../ai-openai/src/utils/structured-output.ts
2
+ /**
3
+ * Map an explicit `responseFormat` override to the default
4
+ * `structuredOutput` capability. Loose wire modes (`"json_object"`,
5
+ * `"text"`) don't enforce shape, so the agent needs to see the soft
6
+ * schema hint in the system prompt — that only happens when the
7
+ * capability is `false`. Default (no override) stays `true` to
8
+ * preserve the prior assumption that OpenAI models support strict
9
+ * structured output.
10
+ */
11
+ function inferStructuredOutput(responseFormat) {
12
+ if (responseFormat === "json_object" || responseFormat === "text") return false;
13
+ return true;
14
+ }
15
+ /**
16
+ * Decide the structured-output mode for a call.
17
+ *
18
+ * When `override` (the `responseFormat` config) is set, it wins: `"text"`
19
+ * emits nothing, `"json_object"` always picks the loose mode, and
20
+ * `"json_schema"` picks strict mode (with the same strict-compatibility
21
+ * safety check - a malformed schema still degrades to `json_object` rather
22
+ * than 400). The override exists because some targets (older OpenAI models,
23
+ * OpenRouter routes, Ollama OpenAI-compat) reject strict `json_schema`
24
+ * outright.
25
+ *
26
+ * When the override is omitted, strict `json_schema` mode (token-level
27
+ * enforcement) is used only when the schema is a proper root-object JSON
28
+ * Schema whose every object lists all of its properties in `required`. For
29
+ * anything else it falls back to loose `json_object` mode. Client-side
30
+ * Standard Schema `validate()` still enforces the full shape.
31
+ */
32
+ function planStructuredOutput(responseSchema, override) {
33
+ if (!responseSchema || override === "text") return { mode: "none" };
34
+ if (override === "json_object") return { mode: "json_object" };
35
+ if (isStrictCompatible(responseSchema)) return {
36
+ mode: "json_schema",
37
+ schema: responseSchema
38
+ };
39
+ return { mode: "json_object" };
40
+ }
41
+ /**
42
+ * OpenAI strict `json_schema` mode requires the root to be a JSON Schema
43
+ * object type (`{ type: "object", properties: ... }`). Anything else
44
+ * (top-level arrays, primitives, unknown shapes) is rejected with a 400
45
+ * before a token is sampled. Checked structurally here so the first call
46
+ * doesn't crash on a malformed extraction.
47
+ */
48
+ function isStrictCompatible(schema) {
49
+ return schema.type === "object" && typeof schema.properties === "object" && schema.properties !== null && isStrictSafeNode(schema);
50
+ }
51
+ /**
52
+ * Recursively check the one strict-mode rule schemas most often trip on:
53
+ * every object must list ALL of its `properties` in `required` (OpenAI
54
+ * strict has no notion of optional - optional fields must be expressed as
55
+ * nullable, e.g. `type: ["string", "null"]`, and still appear in
56
+ * `required`). A schema that violates this anywhere in the tree is NOT sent
57
+ * in strict mode, so a hand-built or optional-bearing schema can't 400 the
58
+ * call ("'required' ... must include every key in properties").
59
+ */
60
+ function isStrictSafeNode(node) {
61
+ if (!node || typeof node !== "object") return true;
62
+ const record = node;
63
+ if (record.type === "object" && record.properties && typeof record.properties === "object") {
64
+ const properties = record.properties;
65
+ const keys = Object.keys(properties);
66
+ const required = Array.isArray(record.required) ? record.required : [];
67
+ if (keys.some((key) => !required.includes(key))) return false;
68
+ for (const key of keys) if (!isStrictSafeNode(properties[key])) return false;
69
+ }
70
+ if (record.items !== void 0 && !isStrictSafeNode(record.items)) return false;
71
+ for (const branch of [
72
+ "anyOf",
73
+ "allOf",
74
+ "oneOf"
75
+ ]) {
76
+ const value = record[branch];
77
+ if (Array.isArray(value) && value.some((sub) => !isStrictSafeNode(sub))) return false;
78
+ }
79
+ return true;
80
+ }
81
+
82
+ //#endregion
83
+ export { inferStructuredOutput, planStructuredOutput };
84
+ //# sourceMappingURL=structured-output.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"structured-output.mjs","names":[],"sources":["../../../../../../../ai-openai/src/utils/structured-output.ts"],"sourcesContent":["import type { OpenAIResponseFormat } from \"../config.type\";\n\n/**\n * How a `responseSchema` should reach the wire, decided once for both the\n * Chat Completions adapter (`response_format`) and the Responses adapter\n * (`text.format`). Only the wire location differs between the two.\n */\nexport type StructuredOutputPlan =\n | { mode: \"none\" }\n | { mode: \"json_object\" }\n | { mode: \"json_schema\"; schema: Record<string, unknown> };\n\n/**\n * Map an explicit `responseFormat` override to the default\n * `structuredOutput` capability. Loose wire modes (`\"json_object\"`,\n * `\"text\"`) don't enforce shape, so the agent needs to see the soft\n * schema hint in the system prompt — that only happens when the\n * capability is `false`. Default (no override) stays `true` to\n * preserve the prior assumption that OpenAI models support strict\n * structured output.\n */\nexport function inferStructuredOutput(\n responseFormat: OpenAIResponseFormat | undefined,\n): boolean {\n if (responseFormat === \"json_object\" || responseFormat === \"text\") {\n return false;\n }\n\n return true;\n}\n\n/**\n * Decide the structured-output mode for a call.\n *\n * When `override` (the `responseFormat` config) is set, it wins: `\"text\"`\n * emits nothing, `\"json_object\"` always picks the loose mode, and\n * `\"json_schema\"` picks strict mode (with the same strict-compatibility\n * safety check - a malformed schema still degrades to `json_object` rather\n * than 400). The override exists because some targets (older OpenAI models,\n * OpenRouter routes, Ollama OpenAI-compat) reject strict `json_schema`\n * outright.\n *\n * When the override is omitted, strict `json_schema` mode (token-level\n * enforcement) is used only when the schema is a proper root-object JSON\n * Schema whose every object lists all of its properties in `required`. For\n * anything else it falls back to loose `json_object` mode. Client-side\n * Standard Schema `validate()` still enforces the full shape.\n */\nexport function planStructuredOutput(\n responseSchema: Record<string, unknown> | undefined,\n override: OpenAIResponseFormat | undefined,\n): StructuredOutputPlan {\n if (!responseSchema || override === \"text\") {\n return { mode: \"none\" };\n }\n\n if (override === \"json_object\") {\n return { mode: \"json_object\" };\n }\n\n // Either auto-select (no override) or explicit `\"json_schema\"`. The\n // strict-compat check still applies in the explicit case - a malformed or\n // non-object schema would 400 before sampling, so degrade to `json_object`.\n if (isStrictCompatible(responseSchema)) {\n return { mode: \"json_schema\", schema: responseSchema };\n }\n\n return { mode: \"json_object\" };\n}\n\n/**\n * OpenAI strict `json_schema` mode requires the root to be a JSON Schema\n * object type (`{ type: \"object\", properties: ... }`). Anything else\n * (top-level arrays, primitives, unknown shapes) is rejected with a 400\n * before a token is sampled. Checked structurally here so the first call\n * doesn't crash on a malformed extraction.\n */\nexport function isStrictCompatible(schema: Record<string, unknown>): boolean {\n return (\n schema.type === \"object\" &&\n typeof schema.properties === \"object\" &&\n schema.properties !== null &&\n isStrictSafeNode(schema)\n );\n}\n\n/**\n * Recursively check the one strict-mode rule schemas most often trip on:\n * every object must list ALL of its `properties` in `required` (OpenAI\n * strict has no notion of optional - optional fields must be expressed as\n * nullable, e.g. `type: [\"string\", \"null\"]`, and still appear in\n * `required`). A schema that violates this anywhere in the tree is NOT sent\n * in strict mode, so a hand-built or optional-bearing schema can't 400 the\n * call (\"'required' ... must include every key in properties\").\n */\nfunction isStrictSafeNode(node: unknown): boolean {\n if (!node || typeof node !== \"object\") {\n return true;\n }\n\n const record = node as Record<string, unknown>;\n\n if (record.type === \"object\" && record.properties && typeof record.properties === \"object\") {\n const properties = record.properties as Record<string, unknown>;\n const keys = Object.keys(properties);\n const required = Array.isArray(record.required) ? (record.required as unknown[]) : [];\n\n if (keys.some((key) => !required.includes(key))) {\n return false;\n }\n\n for (const key of keys) {\n if (!isStrictSafeNode(properties[key])) {\n return false;\n }\n }\n }\n\n if (record.items !== undefined && !isStrictSafeNode(record.items)) {\n return false;\n }\n\n for (const branch of [\"anyOf\", \"allOf\", \"oneOf\"] as const) {\n const value = record[branch];\n\n if (Array.isArray(value) && value.some((sub) => !isStrictSafeNode(sub))) {\n return false;\n }\n }\n\n return true;\n}\n"],"mappings":";;;;;;;;;;AAqBA,SAAgB,sBACd,gBACS;CACT,IAAI,mBAAmB,iBAAiB,mBAAmB,QACzD,OAAO;CAGT,OAAO;AACT;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,qBACd,gBACA,UACsB;CACtB,IAAI,CAAC,kBAAkB,aAAa,QAClC,OAAO,EAAE,MAAM,OAAO;CAGxB,IAAI,aAAa,eACf,OAAO,EAAE,MAAM,cAAc;CAM/B,IAAI,mBAAmB,cAAc,GACnC,OAAO;EAAE,MAAM;EAAe,QAAQ;CAAe;CAGvD,OAAO,EAAE,MAAM,cAAc;AAC/B;;;;;;;;AASA,SAAgB,mBAAmB,QAA0C;CAC3E,OACE,OAAO,SAAS,YAChB,OAAO,OAAO,eAAe,YAC7B,OAAO,eAAe,QACtB,iBAAiB,MAAM;AAE3B;;;;;;;;;;AAWA,SAAS,iBAAiB,MAAwB;CAChD,IAAI,CAAC,QAAQ,OAAO,SAAS,UAC3B,OAAO;CAGT,MAAM,SAAS;CAEf,IAAI,OAAO,SAAS,YAAY,OAAO,cAAc,OAAO,OAAO,eAAe,UAAU;EAC1F,MAAM,aAAa,OAAO;EAC1B,MAAM,OAAO,OAAO,KAAK,UAAU;EACnC,MAAM,WAAW,MAAM,QAAQ,OAAO,QAAQ,IAAK,OAAO,WAAyB,CAAC;EAEpF,IAAI,KAAK,MAAM,QAAQ,CAAC,SAAS,SAAS,GAAG,CAAC,GAC5C,OAAO;EAGT,KAAK,MAAM,OAAO,MAChB,IAAI,CAAC,iBAAiB,WAAW,IAAI,GACnC,OAAO;CAGb;CAEA,IAAI,OAAO,UAAU,UAAa,CAAC,iBAAiB,OAAO,KAAK,GAC9D,OAAO;CAGT,KAAK,MAAM,UAAU;EAAC;EAAS;EAAS;CAAO,GAAY;EACzD,MAAM,QAAQ,OAAO;EAErB,IAAI,MAAM,QAAQ,KAAK,KAAK,MAAM,MAAM,QAAQ,CAAC,iBAAiB,GAAG,CAAC,GACpE,OAAO;CAEX;CAEA,OAAO;AACT"}
@@ -127,7 +127,7 @@ function toOpenAIContentPart(part) {
127
127
  };
128
128
  if (part.type === "image") return {
129
129
  type: "image_url",
130
- image_url: { url: "url" in part.source ? part.source.url : `data:${part.source.mediaType};base64,${part.source.base64}` }
130
+ image_url: { url: toImageUrl(part.source) }
131
131
  };
132
132
  if (part.type === "pdf") {
133
133
  if ("url" in part.source) throw new InvalidRequestError("OpenAI chat completions cannot fetch a remote-URL PDF; supply base64 document bytes instead.");
@@ -149,6 +149,13 @@ function toOpenAIContentPart(part) {
149
149
  };
150
150
  }
151
151
  /**
152
+ * Render an image source as a URL: a remote URL passes through, inlined
153
+ * base64 bytes become a `data:` URL. Shared with the Responses adapter.
154
+ */
155
+ function toImageUrl(source) {
156
+ return "url" in source ? source.url : `data:${source.mediaType};base64,${source.base64}`;
157
+ }
158
+ /**
152
159
  * Narrow a neutral audio media type to the two formats OpenAI's
153
160
  * `input_audio` accepts (`wav` / `mp3`). An unsupported type raises a
154
161
  * typed `InvalidRequestError` up front rather than a provider 400.
@@ -160,5 +167,5 @@ function toOpenAIAudioFormat(mediaType) {
160
167
  }
161
168
 
162
169
  //#endregion
163
- export { toOpenAIMessages };
170
+ export { stringifyContent, toImageUrl, toOpenAIMessages };
164
171
  //# sourceMappingURL=to-openai-messages.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"to-openai-messages.mjs","names":[],"sources":["../../../../../../../ai-openai/src/utils/to-openai-messages.ts"],"sourcesContent":["import { InvalidRequestError, type ContentPart, type Message } from \"@warlock.js/ai\";\nimport type OpenAI from \"openai\";\n\n/**\n * Convert vendor-neutral Message[] to OpenAI's chat message shape.\n * Handles the `tool` role (requires `tool_call_id`) and assistant messages\n * that carry `toolCalls` from a prior model response.\n *\n * Multipart `content` (a `ContentPart[]`) is mapped into OpenAI's user-message\n * content-parts shape: text becomes `{ type: \"text\", text }`, images become\n * `{ type: \"image_url\", image_url: { url } }` — with base64 sources rendered\n * as `data:` URLs inline.\n *\n * @example\n * const openaiMessages = toOpenAIMessages([\n * { role: \"user\", content: \"Hi\" },\n * { role: \"tool\", toolCallId: \"call_1\", content: '{\"ok\":true}' },\n * ]);\n *\n * @example\n * toOpenAIMessages([\n * { role: \"user\", content: [\n * { type: \"text\", text: \"What is this?\" },\n * { type: \"image\", source: { url: \"https://example.com/cat.jpg\" } },\n * ]},\n * ]);\n */\nexport function toOpenAIMessages(\n messages: Message[],\n cacheBreakpoints: number = 0,\n): OpenAI.Chat.Completions.ChatCompletionMessageParam[] {\n const mapped = messages.map((m) => {\n if (m.role === \"tool\") {\n return {\n role: \"tool\",\n content: stringifyContent(m.content),\n tool_call_id: m.toolCallId ?? \"\",\n } satisfies OpenAI.Chat.Completions.ChatCompletionToolMessageParam;\n }\n if (m.role === \"assistant\" && m.toolCalls && m.toolCalls.length > 0) {\n return {\n role: \"assistant\",\n content: stringifyContent(m.content),\n tool_calls: m.toolCalls.map((tc) => ({\n id: tc.id,\n type: \"function\" as const,\n function: { name: tc.name, arguments: JSON.stringify(tc.input ?? {}) },\n })),\n } satisfies OpenAI.Chat.Completions.ChatCompletionAssistantMessageParam;\n }\n\n if (m.role === \"user\" && Array.isArray(m.content)) {\n return {\n role: \"user\",\n content: m.content.map(toOpenAIContentPart),\n } satisfies OpenAI.Chat.Completions.ChatCompletionUserMessageParam;\n }\n\n return { role: m.role, content: stringifyContent(m.content) } as\n | OpenAI.Chat.Completions.ChatCompletionUserMessageParam\n | OpenAI.Chat.Completions.ChatCompletionSystemMessageParam\n | OpenAI.Chat.Completions.ChatCompletionAssistantMessageParam;\n });\n\n return addPromptCacheBreakpoints(mapped, cacheBreakpoints);\n}\n\n/** Place explicit markers on the last requested user-content parts (maximum four). */\nfunction addPromptCacheBreakpoints(\n messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[],\n requested: number,\n): OpenAI.Chat.Completions.ChatCompletionMessageParam[] {\n let remaining = Math.min(4, Math.max(0, Math.floor(requested)));\n if (remaining === 0) return messages;\n\n const marked = [...messages];\n for (let index = marked.length - 1; index >= 0 && remaining > 0; index--) {\n const message = marked[index];\n if (message === undefined || message.role !== \"user\") continue;\n\n if (typeof message.content === \"string\") {\n marked[index] = {\n ...message,\n content: [\n { type: \"text\", text: message.content, prompt_cache_breakpoint: { mode: \"explicit\" } },\n ],\n };\n remaining--;\n continue;\n }\n\n const content = [...message.content];\n for (let partIndex = content.length - 1; partIndex >= 0 && remaining > 0; partIndex--) {\n const part = content[partIndex];\n if (part === undefined) continue;\n content[partIndex] = { ...part, prompt_cache_breakpoint: { mode: \"explicit\" } };\n remaining--;\n }\n marked[index] = { ...message, content };\n }\n\n return marked;\n}\n\n/**\n * Multipart content is only meaningful on user messages — for any other\n * role (system / assistant text / tool), collapse a `ContentPart[]` to\n * its concatenated text so OpenAI's wire format stays valid. Plain\n * strings pass through unchanged.\n */\nfunction stringifyContent(content: string | ContentPart[]): string {\n if (typeof content === \"string\") {\n return content;\n }\n\n return content\n .filter((part): part is { type: \"text\"; text: string } => part.type === \"text\")\n .map((part) => part.text)\n .join(\"\");\n}\n\n/**\n * Map a resolved `ContentPart` to an OpenAI chat content part — one\n * branch per modality, each to its real wire shape:\n *\n * - `text` → `{ type: \"text\" }`.\n * - `image` → `{ type: \"image_url\" }` (remote URL, or a `data:` URL for\n * inlined base64 bytes).\n * - `pdf` → `{ type: \"file\", file: { file_data } }` (OpenAI document\n * input; base64 only — there is no remote-URL file source).\n * - `audio` → `{ type: \"input_audio\", input_audio: { data, format } }`\n * (base64 only; `wav` / `mp3` are the only formats OpenAI accepts).\n *\n * PDF and audio reach this point ONLY when the model declared the\n * matching capability (`openai.model({ name, pdf: true })` /\n * `{ audio: true }`) — the agent's modality gate throws upfront\n * otherwise, so capability and behavior stay in lockstep. A remote-URL\n * pdf/audio source raises a typed `InvalidRequestError` here rather\n * than a downstream provider fault.\n */\nfunction toOpenAIContentPart(part: ContentPart): OpenAI.Chat.Completions.ChatCompletionContentPart {\n if (part.type === \"text\") {\n return { type: \"text\", text: part.text };\n }\n\n if (part.type === \"image\") {\n const url =\n \"url\" in part.source\n ? part.source.url\n : `data:${part.source.mediaType};base64,${part.source.base64}`;\n\n return { type: \"image_url\", image_url: { url } };\n }\n\n if (part.type === \"pdf\") {\n if (\"url\" in part.source) {\n throw new InvalidRequestError(\n \"OpenAI chat completions cannot fetch a remote-URL PDF; supply base64 document bytes instead.\",\n );\n }\n\n return {\n type: \"file\",\n file: {\n filename: \"document.pdf\",\n file_data: `data:${part.source.mediaType};base64,${part.source.base64}`,\n },\n };\n }\n\n // Audio — the remaining `ContentPart` variant.\n if (\"url\" in part.source) {\n throw new InvalidRequestError(\n \"OpenAI chat completions cannot fetch remote-URL audio; supply base64 audio bytes instead.\",\n );\n }\n\n return {\n type: \"input_audio\",\n input_audio: {\n data: part.source.base64,\n format: toOpenAIAudioFormat(part.source.mediaType),\n },\n };\n}\n\n/**\n * Narrow a neutral audio media type to the two formats OpenAI's\n * `input_audio` accepts (`wav` / `mp3`). An unsupported type raises a\n * typed `InvalidRequestError` up front rather than a provider 400.\n */\nfunction toOpenAIAudioFormat(mediaType: string): \"wav\" | \"mp3\" {\n if (mediaType === \"audio/wav\" || mediaType === \"audio/x-wav\" || mediaType === \"audio/wave\") {\n return \"wav\";\n }\n\n if (mediaType === \"audio/mp3\" || mediaType === \"audio/mpeg\" || mediaType === \"audio/mpga\") {\n return \"mp3\";\n }\n\n throw new InvalidRequestError(\n `OpenAI input_audio supports only \"wav\" and \"mp3\"; got \"${mediaType}\".`,\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"to-openai-messages.mjs","names":[],"sources":["../../../../../../../ai-openai/src/utils/to-openai-messages.ts"],"sourcesContent":["import { InvalidRequestError, type ContentPart, type Message } from \"@warlock.js/ai\";\nimport type OpenAI from \"openai\";\n\n/**\n * Convert vendor-neutral Message[] to OpenAI's chat message shape.\n * Handles the `tool` role (requires `tool_call_id`) and assistant messages\n * that carry `toolCalls` from a prior model response.\n *\n * Multipart `content` (a `ContentPart[]`) is mapped into OpenAI's user-message\n * content-parts shape: text becomes `{ type: \"text\", text }`, images become\n * `{ type: \"image_url\", image_url: { url } }` — with base64 sources rendered\n * as `data:` URLs inline.\n *\n * @example\n * const openaiMessages = toOpenAIMessages([\n * { role: \"user\", content: \"Hi\" },\n * { role: \"tool\", toolCallId: \"call_1\", content: '{\"ok\":true}' },\n * ]);\n *\n * @example\n * toOpenAIMessages([\n * { role: \"user\", content: [\n * { type: \"text\", text: \"What is this?\" },\n * { type: \"image\", source: { url: \"https://example.com/cat.jpg\" } },\n * ]},\n * ]);\n */\nexport function toOpenAIMessages(\n messages: Message[],\n cacheBreakpoints: number = 0,\n): OpenAI.Chat.Completions.ChatCompletionMessageParam[] {\n const mapped = messages.map((m) => {\n if (m.role === \"tool\") {\n return {\n role: \"tool\",\n content: stringifyContent(m.content),\n tool_call_id: m.toolCallId ?? \"\",\n } satisfies OpenAI.Chat.Completions.ChatCompletionToolMessageParam;\n }\n if (m.role === \"assistant\" && m.toolCalls && m.toolCalls.length > 0) {\n return {\n role: \"assistant\",\n content: stringifyContent(m.content),\n tool_calls: m.toolCalls.map((tc) => ({\n id: tc.id,\n type: \"function\" as const,\n function: { name: tc.name, arguments: JSON.stringify(tc.input ?? {}) },\n })),\n } satisfies OpenAI.Chat.Completions.ChatCompletionAssistantMessageParam;\n }\n\n if (m.role === \"user\" && Array.isArray(m.content)) {\n return {\n role: \"user\",\n content: m.content.map(toOpenAIContentPart),\n } satisfies OpenAI.Chat.Completions.ChatCompletionUserMessageParam;\n }\n\n return { role: m.role, content: stringifyContent(m.content) } as\n | OpenAI.Chat.Completions.ChatCompletionUserMessageParam\n | OpenAI.Chat.Completions.ChatCompletionSystemMessageParam\n | OpenAI.Chat.Completions.ChatCompletionAssistantMessageParam;\n });\n\n return addPromptCacheBreakpoints(mapped, cacheBreakpoints);\n}\n\n/** Place explicit markers on the last requested user-content parts (maximum four). */\nfunction addPromptCacheBreakpoints(\n messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[],\n requested: number,\n): OpenAI.Chat.Completions.ChatCompletionMessageParam[] {\n let remaining = Math.min(4, Math.max(0, Math.floor(requested)));\n if (remaining === 0) return messages;\n\n const marked = [...messages];\n for (let index = marked.length - 1; index >= 0 && remaining > 0; index--) {\n const message = marked[index];\n if (message === undefined || message.role !== \"user\") continue;\n\n if (typeof message.content === \"string\") {\n marked[index] = {\n ...message,\n content: [\n { type: \"text\", text: message.content, prompt_cache_breakpoint: { mode: \"explicit\" } },\n ],\n };\n remaining--;\n continue;\n }\n\n const content = [...message.content];\n for (let partIndex = content.length - 1; partIndex >= 0 && remaining > 0; partIndex--) {\n const part = content[partIndex];\n if (part === undefined) continue;\n content[partIndex] = { ...part, prompt_cache_breakpoint: { mode: \"explicit\" } };\n remaining--;\n }\n marked[index] = { ...message, content };\n }\n\n return marked;\n}\n\n/**\n * Multipart content is only meaningful on user messages — for any other\n * role (system / assistant text / tool), collapse a `ContentPart[]` to\n * its concatenated text so OpenAI's wire format stays valid. Plain\n * strings pass through unchanged.\n */\nexport function stringifyContent(content: string | ContentPart[]): string {\n if (typeof content === \"string\") {\n return content;\n }\n\n return content\n .filter((part): part is { type: \"text\"; text: string } => part.type === \"text\")\n .map((part) => part.text)\n .join(\"\");\n}\n\n/**\n * Map a resolved `ContentPart` to an OpenAI chat content part — one\n * branch per modality, each to its real wire shape:\n *\n * - `text` → `{ type: \"text\" }`.\n * - `image` → `{ type: \"image_url\" }` (remote URL, or a `data:` URL for\n * inlined base64 bytes).\n * - `pdf` → `{ type: \"file\", file: { file_data } }` (OpenAI document\n * input; base64 only — there is no remote-URL file source).\n * - `audio` → `{ type: \"input_audio\", input_audio: { data, format } }`\n * (base64 only; `wav` / `mp3` are the only formats OpenAI accepts).\n *\n * PDF and audio reach this point ONLY when the model declared the\n * matching capability (`openai.model({ name, pdf: true })` /\n * `{ audio: true }`) — the agent's modality gate throws upfront\n * otherwise, so capability and behavior stay in lockstep. A remote-URL\n * pdf/audio source raises a typed `InvalidRequestError` here rather\n * than a downstream provider fault.\n */\nfunction toOpenAIContentPart(part: ContentPart): OpenAI.Chat.Completions.ChatCompletionContentPart {\n if (part.type === \"text\") {\n return { type: \"text\", text: part.text };\n }\n\n if (part.type === \"image\") {\n return { type: \"image_url\", image_url: { url: toImageUrl(part.source) } };\n }\n\n if (part.type === \"pdf\") {\n if (\"url\" in part.source) {\n throw new InvalidRequestError(\n \"OpenAI chat completions cannot fetch a remote-URL PDF; supply base64 document bytes instead.\",\n );\n }\n\n return {\n type: \"file\",\n file: {\n filename: \"document.pdf\",\n file_data: `data:${part.source.mediaType};base64,${part.source.base64}`,\n },\n };\n }\n\n // Audio — the remaining `ContentPart` variant.\n if (\"url\" in part.source) {\n throw new InvalidRequestError(\n \"OpenAI chat completions cannot fetch remote-URL audio; supply base64 audio bytes instead.\",\n );\n }\n\n return {\n type: \"input_audio\",\n input_audio: {\n data: part.source.base64,\n format: toOpenAIAudioFormat(part.source.mediaType),\n },\n };\n}\n\n/**\n * Render an image source as a URL: a remote URL passes through, inlined\n * base64 bytes become a `data:` URL. Shared with the Responses adapter.\n */\nexport function toImageUrl(\n source: Extract<ContentPart, { type: \"image\" }>[\"source\"],\n): string {\n return \"url\" in source ? source.url : `data:${source.mediaType};base64,${source.base64}`;\n}\n\n/**\n * Narrow a neutral audio media type to the two formats OpenAI's\n * `input_audio` accepts (`wav` / `mp3`). An unsupported type raises a\n * typed `InvalidRequestError` up front rather than a provider 400.\n */\nfunction toOpenAIAudioFormat(mediaType: string): \"wav\" | \"mp3\" {\n if (mediaType === \"audio/wav\" || mediaType === \"audio/x-wav\" || mediaType === \"audio/wave\") {\n return \"wav\";\n }\n\n if (mediaType === \"audio/mp3\" || mediaType === \"audio/mpeg\" || mediaType === \"audio/mpga\") {\n return \"mp3\";\n }\n\n throw new InvalidRequestError(\n `OpenAI input_audio supports only \"wav\" and \"mp3\"; got \"${mediaType}\".`,\n );\n}\n"],"mappings":"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"}
@@ -17,7 +17,7 @@ function toOpenAITools(tools) {
17
17
  function: {
18
18
  name: tool.name,
19
19
  description: tool.description,
20
- parameters: toParameters(tool.input)
20
+ parameters: toToolParameters(tool.input)
21
21
  }
22
22
  }));
23
23
  }
@@ -27,7 +27,7 @@ function toOpenAITools(tools) {
27
27
  * failed extraction) degrades to an empty-object schema so the tool
28
28
  * still registers and the model simply sees no parameters.
29
29
  */
30
- function toParameters(input) {
30
+ function toToolParameters(input) {
31
31
  const schema = extractJsonSchema(input);
32
32
  if (schema && schema.type === "object") return schema;
33
33
  return {
@@ -37,5 +37,5 @@ function toParameters(input) {
37
37
  }
38
38
 
39
39
  //#endregion
40
- export { toOpenAITools };
40
+ export { toOpenAITools, toToolParameters };
41
41
  //# sourceMappingURL=to-openai-tools.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"to-openai-tools.mjs","names":[],"sources":["../../../../../../../ai-openai/src/utils/to-openai-tools.ts"],"sourcesContent":["import { extractJsonSchema, type ToolConfig } from \"@warlock.js/ai\";\nimport type OpenAI from \"openai\";\n\n/**\n * Convert vendor-neutral ToolConfig[] to OpenAI's tools array.\n * Uses the shared `extractJsonSchema` helper; falls back to an empty-object\n * schema when extraction fails so the tool still registers with the provider.\n *\n * @example\n * const tools = toOpenAITools([weatherTool, calculatorTool]);\n * await client.chat.completions.create({ model, messages, tools });\n */\nexport function toOpenAITools(\n tools: ToolConfig<unknown, unknown>[] | undefined,\n): OpenAI.Chat.Completions.ChatCompletionTool[] | undefined {\n if (!tools || tools.length === 0) {\n return undefined;\n }\n\n return tools.map((tool) => ({\n type: \"function\",\n function: {\n name: tool.name,\n description: tool.description,\n parameters: toParameters(tool.input),\n },\n }));\n}\n\n/**\n * Resolve a tool's input schema to a JSON-Schema object. OpenAI's\n * function `parameters` expects an object root; anything else (or a\n * failed extraction) degrades to an empty-object schema so the tool\n * still registers and the model simply sees no parameters.\n */\nfunction toParameters(input: ToolConfig<unknown, unknown>[\"input\"]): Record<string, unknown> {\n const schema = extractJsonSchema(input);\n\n if (schema && schema.type === \"object\") {\n return schema;\n }\n\n return { type: \"object\", properties: {} };\n}\n"],"mappings":";;;;;;;;;;;;AAYA,SAAgB,cACd,OAC0D;CAC1D,IAAI,CAAC,SAAS,MAAM,WAAW,GAC7B;CAGF,OAAO,MAAM,KAAK,UAAU;EAC1B,MAAM;EACN,UAAU;GACR,MAAM,KAAK;GACX,aAAa,KAAK;GAClB,YAAY,aAAa,KAAK,KAAK;EACrC;CACF,EAAE;AACJ;;;;;;;AAQA,SAAS,aAAa,OAAuE;CAC3F,MAAM,SAAS,kBAAkB,KAAK;CAEtC,IAAI,UAAU,OAAO,SAAS,UAC5B,OAAO;CAGT,OAAO;EAAE,MAAM;EAAU,YAAY,CAAC;CAAE;AAC1C"}
1
+ {"version":3,"file":"to-openai-tools.mjs","names":[],"sources":["../../../../../../../ai-openai/src/utils/to-openai-tools.ts"],"sourcesContent":["import { extractJsonSchema, type ToolConfig } from \"@warlock.js/ai\";\nimport type OpenAI from \"openai\";\n\n/**\n * Convert vendor-neutral ToolConfig[] to OpenAI's tools array.\n * Uses the shared `extractJsonSchema` helper; falls back to an empty-object\n * schema when extraction fails so the tool still registers with the provider.\n *\n * @example\n * const tools = toOpenAITools([weatherTool, calculatorTool]);\n * await client.chat.completions.create({ model, messages, tools });\n */\nexport function toOpenAITools(\n tools: ToolConfig<unknown, unknown>[] | undefined,\n): OpenAI.Chat.Completions.ChatCompletionTool[] | undefined {\n if (!tools || tools.length === 0) {\n return undefined;\n }\n\n return tools.map((tool) => ({\n type: \"function\",\n function: {\n name: tool.name,\n description: tool.description,\n parameters: toToolParameters(tool.input),\n },\n }));\n}\n\n/**\n * Resolve a tool's input schema to a JSON-Schema object. OpenAI's\n * function `parameters` expects an object root; anything else (or a\n * failed extraction) degrades to an empty-object schema so the tool\n * still registers and the model simply sees no parameters.\n */\nexport function toToolParameters(input: ToolConfig<unknown, unknown>[\"input\"]): Record<string, unknown> {\n const schema = extractJsonSchema(input);\n\n if (schema && schema.type === \"object\") {\n return schema;\n }\n\n return { type: \"object\", properties: {} };\n}\n"],"mappings":";;;;;;;;;;;;AAYA,SAAgB,cACd,OAC0D;CAC1D,IAAI,CAAC,SAAS,MAAM,WAAW,GAC7B;CAGF,OAAO,MAAM,KAAK,UAAU;EAC1B,MAAM;EACN,UAAU;GACR,MAAM,KAAK;GACX,aAAa,KAAK;GAClB,YAAY,iBAAiB,KAAK,KAAK;EACzC;CACF,EAAE;AACJ;;;;;;;AAQA,SAAgB,iBAAiB,OAAuE;CACtG,MAAM,SAAS,kBAAkB,KAAK;CAEtC,IAAI,UAAU,OAAO,SAAS,UAC5B,OAAO;CAGT,OAAO;EAAE,MAAM;EAAU,YAAY,CAAC;CAAE;AAC1C"}
@@ -0,0 +1,112 @@
1
+ import { readReasoningReplay, toReplayReasoningItem } from "./reasoning-replay.mjs";
2
+ import { stringifyContent, toImageUrl } from "./to-openai-messages.mjs";
3
+ import { InvalidRequestError } from "@warlock.js/ai";
4
+
5
+ //#region ../ai-openai/src/utils/to-responses-input.ts
6
+ /**
7
+ * Convert vendor-neutral `Message[]` into the Responses request pieces.
8
+ *
9
+ * - The FIRST system message becomes `instructions` (decision 9: it avoids
10
+ * the `system` vs `developer` role question on reasoning models). Any
11
+ * later system message stays inline as a `system` role item.
12
+ * - `user` / `assistant` text becomes an `EasyInputMessage` (`{ role,
13
+ * content }`). Multipart user content becomes `input_text` / `input_image`
14
+ * parts; images always carry `detail: "auto"` because `ResponseInputImage.detail`
15
+ * is required by the SDK types.
16
+ * - An assistant message that requested tools becomes its text (when
17
+ * non-empty) followed by one top-level `function_call` item per call; our
18
+ * `ModelToolCallRequest.id` is the wire `call_id`. No item `id` is sent:
19
+ * with `store: false` nothing was persisted for it to refer to.
20
+ * - When `model` is given, reasoning items stored on the turn's tool calls
21
+ * (`providerMetadata.openaiResponses`, see `reasoning-replay.ts`) are
22
+ * emitted BEFORE the turn's text and `function_call` items (the order the
23
+ * API produced them), only if they were produced by that same model.
24
+ * Without `model` nothing is replayed.
25
+ * - A `tool` message becomes a top-level `function_call_output` item.
26
+ *
27
+ * PDF and audio parts are rejected with a typed `InvalidRequestError`: the
28
+ * Responses adapter declares those capabilities off, so they only arrive
29
+ * here if a caller bypasses the agent's capability gate.
30
+ *
31
+ * @example
32
+ * toResponsesInput([
33
+ * { role: "system", content: "Be brief." },
34
+ * { role: "user", content: "Hi" },
35
+ * ]);
36
+ * // { instructions: "Be brief.", input: [{ role: "user", content: "Hi" }] }
37
+ */
38
+ function toResponsesInput(messages, model) {
39
+ const input = [];
40
+ let instructions;
41
+ let instructionsTaken = false;
42
+ for (const message of messages) {
43
+ if (message.role === "system") {
44
+ const text = stringifyContent(message.content);
45
+ if (!instructionsTaken) {
46
+ instructionsTaken = true;
47
+ instructions = text === "" ? void 0 : text;
48
+ continue;
49
+ }
50
+ input.push({
51
+ role: "system",
52
+ content: text
53
+ });
54
+ continue;
55
+ }
56
+ if (message.role === "tool") {
57
+ input.push({
58
+ type: "function_call_output",
59
+ call_id: message.toolCallId ?? "",
60
+ output: stringifyContent(message.content)
61
+ });
62
+ continue;
63
+ }
64
+ if (message.role === "assistant" && message.toolCalls && message.toolCalls.length > 0) {
65
+ const text = stringifyContent(message.content);
66
+ if (model !== void 0) for (const item of readReasoningReplay(message.toolCalls, model)) input.push(toReplayReasoningItem(item));
67
+ if (text !== "") input.push({
68
+ role: "assistant",
69
+ content: text
70
+ });
71
+ for (const call of message.toolCalls) input.push({
72
+ type: "function_call",
73
+ call_id: call.id,
74
+ name: call.name,
75
+ arguments: JSON.stringify(call.input ?? {})
76
+ });
77
+ continue;
78
+ }
79
+ if (message.role === "user" && Array.isArray(message.content)) {
80
+ input.push({
81
+ role: "user",
82
+ content: message.content.map(toResponsesContentPart)
83
+ });
84
+ continue;
85
+ }
86
+ input.push({
87
+ role: message.role,
88
+ content: stringifyContent(message.content)
89
+ });
90
+ }
91
+ return {
92
+ ...instructions !== void 0 ? { instructions } : {},
93
+ input
94
+ };
95
+ }
96
+ /** Map a resolved `ContentPart` to a Responses input content part (text and images only). */
97
+ function toResponsesContentPart(part) {
98
+ if (part.type === "text") return {
99
+ type: "input_text",
100
+ text: part.text
101
+ };
102
+ if (part.type === "image") return {
103
+ type: "input_image",
104
+ image_url: toImageUrl(part.source),
105
+ detail: "auto"
106
+ };
107
+ throw new InvalidRequestError(`OpenAI api: "responses" does not accept ${part.type} input in this adapter; use api: "chat" for ${part.type} attachments.`);
108
+ }
109
+
110
+ //#endregion
111
+ export { toResponsesInput };
112
+ //# sourceMappingURL=to-responses-input.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"to-responses-input.mjs","names":[],"sources":["../../../../../../../ai-openai/src/utils/to-responses-input.ts"],"sourcesContent":["import { InvalidRequestError, type ContentPart, type Message } from \"@warlock.js/ai\";\nimport type OpenAI from \"openai\";\nimport { stringifyContent, toImageUrl } from \"./to-openai-messages\";\nimport { readReasoningReplay, toReplayReasoningItem } from \"./reasoning-replay\";\n\n/** One item of the Responses `input` array. */\nexport type ResponsesInputItem = OpenAI.Responses.ResponseInputItem;\n\n/** What `toResponsesInput` produces: the `instructions` string plus the `input` items. */\nexport type ResponsesInputPlan = {\n /** The system prompt, sent as the top-level `instructions` field. */\n instructions?: string;\n input: ResponsesInputItem[];\n};\n\n/**\n * Convert vendor-neutral `Message[]` into the Responses request pieces.\n *\n * - The FIRST system message becomes `instructions` (decision 9: it avoids\n * the `system` vs `developer` role question on reasoning models). Any\n * later system message stays inline as a `system` role item.\n * - `user` / `assistant` text becomes an `EasyInputMessage` (`{ role,\n * content }`). Multipart user content becomes `input_text` / `input_image`\n * parts; images always carry `detail: \"auto\"` because `ResponseInputImage.detail`\n * is required by the SDK types.\n * - An assistant message that requested tools becomes its text (when\n * non-empty) followed by one top-level `function_call` item per call; our\n * `ModelToolCallRequest.id` is the wire `call_id`. No item `id` is sent:\n * with `store: false` nothing was persisted for it to refer to.\n * - When `model` is given, reasoning items stored on the turn's tool calls\n * (`providerMetadata.openaiResponses`, see `reasoning-replay.ts`) are\n * emitted BEFORE the turn's text and `function_call` items (the order the\n * API produced them), only if they were produced by that same model.\n * Without `model` nothing is replayed.\n * - A `tool` message becomes a top-level `function_call_output` item.\n *\n * PDF and audio parts are rejected with a typed `InvalidRequestError`: the\n * Responses adapter declares those capabilities off, so they only arrive\n * here if a caller bypasses the agent's capability gate.\n *\n * @example\n * toResponsesInput([\n * { role: \"system\", content: \"Be brief.\" },\n * { role: \"user\", content: \"Hi\" },\n * ]);\n * // { instructions: \"Be brief.\", input: [{ role: \"user\", content: \"Hi\" }] }\n */\nexport function toResponsesInput(messages: Message[], model?: string): ResponsesInputPlan {\n const input: ResponsesInputItem[] = [];\n let instructions: string | undefined;\n let instructionsTaken = false;\n\n for (const message of messages) {\n if (message.role === \"system\") {\n const text = stringifyContent(message.content);\n\n if (!instructionsTaken) {\n instructionsTaken = true;\n instructions = text === \"\" ? undefined : text;\n continue;\n }\n\n input.push({ role: \"system\", content: text });\n continue;\n }\n\n if (message.role === \"tool\") {\n input.push({\n type: \"function_call_output\",\n call_id: message.toolCallId ?? \"\",\n output: stringifyContent(message.content),\n });\n continue;\n }\n\n if (message.role === \"assistant\" && message.toolCalls && message.toolCalls.length > 0) {\n const text = stringifyContent(message.content);\n\n if (model !== undefined) {\n for (const item of readReasoningReplay(message.toolCalls, model)) {\n input.push(toReplayReasoningItem(item));\n }\n }\n\n if (text !== \"\") {\n input.push({ role: \"assistant\", content: text });\n }\n\n for (const call of message.toolCalls) {\n input.push({\n type: \"function_call\",\n call_id: call.id,\n name: call.name,\n arguments: JSON.stringify(call.input ?? {}),\n });\n }\n\n continue;\n }\n\n if (message.role === \"user\" && Array.isArray(message.content)) {\n input.push({ role: \"user\", content: message.content.map(toResponsesContentPart) });\n continue;\n }\n\n input.push({ role: message.role, content: stringifyContent(message.content) });\n }\n\n return { ...(instructions !== undefined ? { instructions } : {}), input };\n}\n\n/** Map a resolved `ContentPart` to a Responses input content part (text and images only). */\nfunction toResponsesContentPart(\n part: ContentPart,\n): OpenAI.Responses.ResponseInputText | OpenAI.Responses.ResponseInputImage {\n if (part.type === \"text\") {\n return { type: \"input_text\", text: part.text };\n }\n\n if (part.type === \"image\") {\n return { type: \"input_image\", image_url: toImageUrl(part.source), detail: \"auto\" };\n }\n\n throw new InvalidRequestError(\n `OpenAI api: \"responses\" does not accept ${part.type} input in this adapter; use api: \"chat\" for ${part.type} attachments.`,\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+CA,SAAgB,iBAAiB,UAAqB,OAAoC;CACxF,MAAM,QAA8B,CAAC;CACrC,IAAI;CACJ,IAAI,oBAAoB;CAExB,KAAK,MAAM,WAAW,UAAU;EAC9B,IAAI,QAAQ,SAAS,UAAU;GAC7B,MAAM,OAAO,iBAAiB,QAAQ,OAAO;GAE7C,IAAI,CAAC,mBAAmB;IACtB,oBAAoB;IACpB,eAAe,SAAS,KAAK,SAAY;IACzC;GACF;GAEA,MAAM,KAAK;IAAE,MAAM;IAAU,SAAS;GAAK,CAAC;GAC5C;EACF;EAEA,IAAI,QAAQ,SAAS,QAAQ;GAC3B,MAAM,KAAK;IACT,MAAM;IACN,SAAS,QAAQ,cAAc;IAC/B,QAAQ,iBAAiB,QAAQ,OAAO;GAC1C,CAAC;GACD;EACF;EAEA,IAAI,QAAQ,SAAS,eAAe,QAAQ,aAAa,QAAQ,UAAU,SAAS,GAAG;GACrF,MAAM,OAAO,iBAAiB,QAAQ,OAAO;GAE7C,IAAI,UAAU,QACZ,KAAK,MAAM,QAAQ,oBAAoB,QAAQ,WAAW,KAAK,GAC7D,MAAM,KAAK,sBAAsB,IAAI,CAAC;GAI1C,IAAI,SAAS,IACX,MAAM,KAAK;IAAE,MAAM;IAAa,SAAS;GAAK,CAAC;GAGjD,KAAK,MAAM,QAAQ,QAAQ,WACzB,MAAM,KAAK;IACT,MAAM;IACN,SAAS,KAAK;IACd,MAAM,KAAK;IACX,WAAW,KAAK,UAAU,KAAK,SAAS,CAAC,CAAC;GAC5C,CAAC;GAGH;EACF;EAEA,IAAI,QAAQ,SAAS,UAAU,MAAM,QAAQ,QAAQ,OAAO,GAAG;GAC7D,MAAM,KAAK;IAAE,MAAM;IAAQ,SAAS,QAAQ,QAAQ,IAAI,sBAAsB;GAAE,CAAC;GACjF;EACF;EAEA,MAAM,KAAK;GAAE,MAAM,QAAQ;GAAM,SAAS,iBAAiB,QAAQ,OAAO;EAAE,CAAC;CAC/E;CAEA,OAAO;EAAE,GAAI,iBAAiB,SAAY,EAAE,aAAa,IAAI,CAAC;EAAI;CAAM;AAC1E;;AAGA,SAAS,uBACP,MAC0E;CAC1E,IAAI,KAAK,SAAS,QAChB,OAAO;EAAE,MAAM;EAAc,MAAM,KAAK;CAAK;CAG/C,IAAI,KAAK,SAAS,SAChB,OAAO;EAAE,MAAM;EAAe,WAAW,WAAW,KAAK,MAAM;EAAG,QAAQ;CAAO;CAGnF,MAAM,IAAI,oBACR,2CAA2C,KAAK,KAAK,8CAA8C,KAAK,KAAK,cAC/G;AACF"}
@@ -0,0 +1,51 @@
1
+ import { toToolParameters } from "./to-openai-tools.mjs";
2
+
3
+ //#region ../ai-openai/src/utils/to-responses-tools.ts
4
+ /**
5
+ * Convert vendor-neutral `ToolConfig[]` to Responses function tools.
6
+ *
7
+ * Responses tools are FLAT (`name` / `description` / `parameters` sit beside
8
+ * `type`, not under a nested `function` object) and `strict` is required by
9
+ * the SDK type (`FunctionTool.strict: boolean | null`). The Chat path never
10
+ * sets `strict` (non-strict is the OpenAI default there), so every tool is
11
+ * sent with `strict: false` to keep the same behavior.
12
+ *
13
+ * @example
14
+ * toResponsesTools([weatherTool]);
15
+ * // [{ type: "function", name: "getWeather", description: "...", parameters: {...}, strict: false }]
16
+ */
17
+ function toResponsesTools(tools) {
18
+ if (!tools || tools.length === 0) return;
19
+ return tools.map((tool) => ({
20
+ type: "function",
21
+ name: tool.name,
22
+ description: tool.description,
23
+ parameters: toToolParameters(tool.input),
24
+ strict: false
25
+ }));
26
+ }
27
+ /**
28
+ * Read an optional `toolChoice` call option into the Responses `tool_choice`.
29
+ *
30
+ * `ModelCallOptions` does not declare `toolChoice` today (it only arrives
31
+ * through the open index signature), so the value is validated here rather
32
+ * than trusted. Accepted: `"none" | "auto" | "required"`, or a forced
33
+ * function as `{ name }` / `{ type: "function", name }` (the Chat-style
34
+ * nested `{ type: "function", function: { name } }` is also accepted). The
35
+ * Responses form is flat: `{ type: "function", name }`. Anything else yields
36
+ * `undefined`, leaving the provider default.
37
+ */
38
+ function toResponsesToolChoice(toolChoice) {
39
+ if (toolChoice === "none" || toolChoice === "auto" || toolChoice === "required") return toolChoice;
40
+ if (typeof toolChoice !== "object" || toolChoice === null) return;
41
+ const record = toolChoice;
42
+ const name = typeof record.name === "string" ? record.name : record.function?.name;
43
+ if (typeof name === "string" && name !== "") return {
44
+ type: "function",
45
+ name
46
+ };
47
+ }
48
+
49
+ //#endregion
50
+ export { toResponsesToolChoice, toResponsesTools };
51
+ //# sourceMappingURL=to-responses-tools.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"to-responses-tools.mjs","names":[],"sources":["../../../../../../../ai-openai/src/utils/to-responses-tools.ts"],"sourcesContent":["import type { ToolConfig } from \"@warlock.js/ai\";\nimport type OpenAI from \"openai\";\nimport { toToolParameters } from \"./to-openai-tools\";\n\n/**\n * Convert vendor-neutral `ToolConfig[]` to Responses function tools.\n *\n * Responses tools are FLAT (`name` / `description` / `parameters` sit beside\n * `type`, not under a nested `function` object) and `strict` is required by\n * the SDK type (`FunctionTool.strict: boolean | null`). The Chat path never\n * sets `strict` (non-strict is the OpenAI default there), so every tool is\n * sent with `strict: false` to keep the same behavior.\n *\n * @example\n * toResponsesTools([weatherTool]);\n * // [{ type: \"function\", name: \"getWeather\", description: \"...\", parameters: {...}, strict: false }]\n */\nexport function toResponsesTools(\n tools: ToolConfig<unknown, unknown>[] | undefined,\n): OpenAI.Responses.FunctionTool[] | undefined {\n if (!tools || tools.length === 0) {\n return undefined;\n }\n\n return tools.map((tool) => ({\n type: \"function\",\n name: tool.name,\n description: tool.description,\n parameters: toToolParameters(tool.input),\n strict: false,\n }));\n}\n\n/**\n * Read an optional `toolChoice` call option into the Responses `tool_choice`.\n *\n * `ModelCallOptions` does not declare `toolChoice` today (it only arrives\n * through the open index signature), so the value is validated here rather\n * than trusted. Accepted: `\"none\" | \"auto\" | \"required\"`, or a forced\n * function as `{ name }` / `{ type: \"function\", name }` (the Chat-style\n * nested `{ type: \"function\", function: { name } }` is also accepted). The\n * Responses form is flat: `{ type: \"function\", name }`. Anything else yields\n * `undefined`, leaving the provider default.\n */\nexport function toResponsesToolChoice(\n toolChoice: unknown,\n): OpenAI.Responses.ToolChoiceOptions | OpenAI.Responses.ToolChoiceFunction | undefined {\n if (toolChoice === \"none\" || toolChoice === \"auto\" || toolChoice === \"required\") {\n return toolChoice;\n }\n\n if (typeof toolChoice !== \"object\" || toolChoice === null) {\n return undefined;\n }\n\n const record = toolChoice as { name?: unknown; function?: { name?: unknown } };\n const name = typeof record.name === \"string\" ? record.name : record.function?.name;\n\n if (typeof name === \"string\" && name !== \"\") {\n return { type: \"function\", name };\n }\n\n return undefined;\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAiBA,SAAgB,iBACd,OAC6C;CAC7C,IAAI,CAAC,SAAS,MAAM,WAAW,GAC7B;CAGF,OAAO,MAAM,KAAK,UAAU;EAC1B,MAAM;EACN,MAAM,KAAK;EACX,aAAa,KAAK;EAClB,YAAY,iBAAiB,KAAK,KAAK;EACvC,QAAQ;CACV,EAAE;AACJ;;;;;;;;;;;;AAaA,SAAgB,sBACd,YACsF;CACtF,IAAI,eAAe,UAAU,eAAe,UAAU,eAAe,YACnE,OAAO;CAGT,IAAI,OAAO,eAAe,YAAY,eAAe,MACnD;CAGF,MAAM,SAAS;CACf,MAAM,OAAO,OAAO,OAAO,SAAS,WAAW,OAAO,OAAO,OAAO,UAAU;CAE9E,IAAI,OAAO,SAAS,YAAY,SAAS,IACvC,OAAO;EAAE,MAAM;EAAY;CAAK;AAIpC"}
package/llms-full.txt CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  ---
10
10
  name: setup-openai
11
- description: 'Wire @warlock.js/ai-openai — new OpenAISDK({apiKey, baseURL?, provider?, pricing?}) for OpenAI / Azure / OpenRouter, .model({name, vision?, reasoning?, structuredOutput?, responseFormat?, pdf?, audio?}) for ModelContract, .embedder({name, dimensions?}) for embeddings, .image({name, pricing?}) for gpt-image-*/dall-e-* image generation (via ai.image). PDF input maps to OpenAI file parts (opt-in pdf:true), audio input to input_audio (opt-in audio:true). Triggers: `OpenAISDK`, `.model`, `.embedder`, `.image`, `.embed`, `.embedMany`, `baseURL`, `pricing`, `responseSchema`, `responseFormat`, `reasoning_effort`, `reasoningTokens`, `cachedTokens`, `gpt-image`, `dall-e`, `ai.image`, `pdf input`, `input_audio`, o-series / gpt-5 reasoning, prompt caching; "wire openai into a warlock agent", "configure gpt-4o", "use o3 / gpt-5 reasoning effort", "route through openrouter or azure openai", "openai embeddings with warlock", "generate images with gpt-image / dall-e", "send a pdf / audio to gpt-4o"; typical import `import { OpenAISDK } from "@warlock.js/ai-openai"`. Skip: the ai.image verb surface — `@warlock.js/ai/generate-images/SKILL.md`; agent wiring — `@warlock.js/ai/run-ai-agent/SKILL.md`; adapter comparison — `@warlock.js/ai/pick-ai-provider/SKILL.md`; competing adapters `@warlock.js/ai-anthropic`, `@warlock.js/ai-bedrock`, `@warlock.js/ai-google`, `@warlock.js/ai-ollama`; raw `openai` SDK, Vercel `@ai-sdk/openai`.'
11
+ description: 'Wire @warlock.js/ai-openai — new OpenAISDK({apiKey, baseURL?, provider?, pricing?}) for OpenAI / Azure / OpenRouter, .model({name, vision?, reasoning?, structuredOutput?, responseFormat?, pdf?, audio?}) for ModelContract, .embedder({name, dimensions?}) for embeddings, .image({name, pricing?}) for gpt-image-*/dall-e-* image generation (via ai.image). PDF input maps to OpenAI file parts (opt-in pdf:true), audio input to input_audio (opt-in audio:true). Triggers: `OpenAISDK`, `.model`, `.embedder`, `.image`, `.embed`, `.embedMany`, `baseURL`, `pricing`, `responseSchema`, `responseFormat`, `reasoning_effort`, `reasoningTokens`, `cachedTokens`, `gpt-image`, `dall-e`, `ai.image`, `pdf input`, `input_audio`, o-series / gpt-5 reasoning, `api: "responses"`, Responses API, reasoning with tools, prompt caching; "wire openai into a warlock agent", "configure gpt-4o", "use o3 / gpt-5 reasoning effort", "route through openrouter or azure openai", "openai embeddings with warlock", "generate images with gpt-image / dall-e", "send a pdf / audio to gpt-4o"; typical import `import { OpenAISDK } from "@warlock.js/ai-openai"`. Skip: the ai.image verb surface — `@warlock.js/ai/generate-images/SKILL.md`; agent wiring — `@warlock.js/ai/run-ai-agent/SKILL.md`; adapter comparison — `@warlock.js/ai/pick-ai-provider/SKILL.md`; competing adapters `@warlock.js/ai-anthropic`, `@warlock.js/ai-bedrock`, `@warlock.js/ai-google`, `@warlock.js/ai-ollama`; raw `openai` SDK, Vercel `@ai-sdk/openai`.'
12
12
  ---
13
13
 
14
14
  # `@warlock.js/ai-openai`
@@ -163,9 +163,26 @@ The gpt-6 models (`gpt-6-astra` / `gpt-6-sol` / `gpt-6-luna`) are hard-400 cases
163
163
  400 Function tools with reasoning_effort are not supported for gpt-6-sol in /v1/chat/completions. To use function tools, use /v1/responses or set reasoning_effort to 'none'.
164
164
  ```
165
165
 
166
- Tools work with reasoning off (the adapter's default when tools are present and no reasoning is set). Responses API support, planned for 5.27, lifts this.
166
+ Tools work with reasoning off (the adapter's default when tools are present and no reasoning is set). For reasoning **and** tools together, use the Responses API below.
167
167
 
168
- Calling **without** `tools` is unaffected — `reasoning_effort` is still omitted by default in that case (provider default reasoning, nothing to unblock). Trade-off of the `"none"` default: you lose reasoning on that call. For tool-heavy agent work (function calls + good replies rather than deep analysis) this is the right call — it's the difference between empty replies and working ones. When you need reasoning **and** tools together, use the Responses API (planned).
168
+ Calling **without** `tools` is unaffected — `reasoning_effort` is still omitted by default in that case (provider default reasoning, nothing to unblock). Trade-off of the `"none"` default: you lose reasoning on that call. For tool-heavy agent work (function calls + good replies rather than deep analysis) this is the right call — it's the difference between empty replies and working ones. When you need reasoning **and** tools together, use the Responses API (next section).
169
+
170
+ ### Reasoning + tools: the Responses API (`api: "responses"`)
171
+
172
+ Opt a direct-OpenAI model into `/v1/responses`, where reasoning models accept function tools with reasoning on:
173
+
174
+ ```ts
175
+ const model = openai.model({ name: "gpt-6-sol", api: "responses" });
176
+ await model.complete(messages, { tools, reasoning: { effort: "high" } }); // reasoning stays on, tools work
177
+ ```
178
+
179
+ - Chat Completions stays the default; nothing changes unless you pass `api: "responses"`.
180
+ - Only the direct `openai` provider accepts it. `@warlock.js/ai-deepseek`, `ai-groq`, `ai-xai` and `ai-mistral` (which reuse this adapter with their own `baseURL`) reject `api: "responses"` with `InvalidRequestError`.
181
+ - With tools attached, reasoning uses the provider default instead of being forced to `"none"`: expect more reasoning tokens and latency than the Chat path. Pass `reasoning: { effort }` to choose.
182
+ - The system prompt is sent as `instructions`; requests use `store: false` (no server-side conversation state).
183
+ - Reasoning is carried across a tool loop without server storage: each response asks for encrypted reasoning, which rides on the first tool call of the turn (`providerMetadata.openaiResponses`) and is sent back on the next request when the model is the same. It is persisted with your conversation history.
184
+ - `complete()` and `stream()` both work; streamed chunks follow the same order as the Chat path (deltas, then tool calls, then `done`). Usage reports `reasoningTokens` and `cachedTokens` the same way.
185
+ - `pdf` and `audio` inputs are not available on this path.
169
186
 
170
187
  ## Token usage — what's reported
171
188
 
package/llms.txt CHANGED
@@ -6,4 +6,4 @@
6
6
 
7
7
  ## Skills
8
8
 
9
- - [setup-openai](@warlock.js/ai-openai/setup-openai/SKILL.md): Wire @warlock.js/ai-openai — new OpenAISDK({apiKey, baseURL?, provider?, pricing?}) for OpenAI / Azure / OpenRouter, .model({name, vision?, reasoning?, structuredOutput?, responseFormat?, pdf?, audio?}) for ModelContract, .embedder({name, dimensions?}) for embeddings, .image({name, pricing?}) for gpt-image-*/dall-e-* image generation (via ai.image). PDF input maps to OpenAI file parts (opt-in pdf:true), audio input to input_audio (opt-in audio:true). Triggers: `OpenAISDK`, `.model`, `.embedder`, `.image`, `.embed`, `.embedMany`, `baseURL`, `pricing`, `responseSchema`, `responseFormat`, `reasoning_effort`, `reasoningTokens`, `cachedTokens`, `gpt-image`, `dall-e`, `ai.image`, `pdf input`, `input_audio`, o-series / gpt-5 reasoning, prompt caching; "wire openai into a warlock agent", "configure gpt-4o", "use o3 / gpt-5 reasoning effort", "route through openrouter or azure openai", "openai embeddings with warlock", "generate images with gpt-image / dall-e", "send a pdf / audio to gpt-4o"; typical import `import { OpenAISDK } from "@warlock.js/ai-openai"`. Skip: the ai.image verb surface — `@warlock.js/ai/generate-images/SKILL.md`; agent wiring — `@warlock.js/ai/run-ai-agent/SKILL.md`; adapter comparison — `@warlock.js/ai/pick-ai-provider/SKILL.md`; competing adapters `@warlock.js/ai-anthropic`, `@warlock.js/ai-bedrock`, `@warlock.js/ai-google`, `@warlock.js/ai-ollama`; raw `openai` SDK, Vercel `@ai-sdk/openai`.
9
+ - [setup-openai](@warlock.js/ai-openai/setup-openai/SKILL.md): Wire @warlock.js/ai-openai — new OpenAISDK({apiKey, baseURL?, provider?, pricing?}) for OpenAI / Azure / OpenRouter, .model({name, vision?, reasoning?, structuredOutput?, responseFormat?, pdf?, audio?}) for ModelContract, .embedder({name, dimensions?}) for embeddings, .image({name, pricing?}) for gpt-image-*/dall-e-* image generation (via ai.image). PDF input maps to OpenAI file parts (opt-in pdf:true), audio input to input_audio (opt-in audio:true). Triggers: `OpenAISDK`, `.model`, `.embedder`, `.image`, `.embed`, `.embedMany`, `baseURL`, `pricing`, `responseSchema`, `responseFormat`, `reasoning_effort`, `reasoningTokens`, `cachedTokens`, `gpt-image`, `dall-e`, `ai.image`, `pdf input`, `input_audio`, o-series / gpt-5 reasoning, `api: "responses"`, Responses API, reasoning with tools, prompt caching; "wire openai into a warlock agent", "configure gpt-4o", "use o3 / gpt-5 reasoning effort", "route through openrouter or azure openai", "openai embeddings with warlock", "generate images with gpt-image / dall-e", "send a pdf / audio to gpt-4o"; typical import `import { OpenAISDK } from "@warlock.js/ai-openai"`. Skip: the ai.image verb surface — `@warlock.js/ai/generate-images/SKILL.md`; agent wiring — `@warlock.js/ai/run-ai-agent/SKILL.md`; adapter comparison — `@warlock.js/ai/pick-ai-provider/SKILL.md`; competing adapters `@warlock.js/ai-anthropic`, `@warlock.js/ai-bedrock`, `@warlock.js/ai-google`, `@warlock.js/ai-ollama`; raw `openai` SDK, Vercel `@ai-sdk/openai`.
package/package.json CHANGED
@@ -14,12 +14,12 @@
14
14
  },
15
15
  "dependencies": {
16
16
  "openai": "^7.23.0",
17
- "@warlock.js/logger": "5.26.0"
17
+ "@warlock.js/logger": "5.28.0"
18
18
  },
19
19
  "peerDependencies": {
20
- "@warlock.js/ai": "5.26.0"
20
+ "@warlock.js/ai": "5.28.0"
21
21
  },
22
- "version": "5.26.0",
22
+ "version": "5.28.0",
23
23
  "main": "./cjs/index.cjs",
24
24
  "module": "./esm/index.mjs",
25
25
  "types": "./esm/index.d.mts",