@warlock.js/ai-openai 4.2.11 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,7 +4,21 @@ All notable changes to `@warlock.js/ai-openai` are documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). `@warlock.js/*` packages are released in lockstep — every package shares the same version number, so a version below may list only the changes that affected this package.
6
6
 
7
- ## [Unreleased]
7
+ ## 4.4.0 - 2026-06-21
8
+
9
+ ### Fixed
10
+
11
+ - **Strict structured-output compatibility check is now recursive.** `json_schema` (strict) mode is used only when every object in the schema lists all of its properties in `required` — OpenAI strict has no optional fields, so a schema that omits one anywhere in the tree would `400`. Such schemas now degrade to loose `json_object` instead of failing the call; client-side validation still enforces the full shape.
12
+
13
+ ## 4.3.0 - 2026-06-21
14
+
15
+ ### Added
16
+
17
+ - Cost-truth wiring (additive, non-breaking):
18
+ - `Usage.reasoningTokens` now populated from `completion_tokens_details.reasoning_tokens` on both `complete()` and the streaming `done` event (o-series / gpt-5 hidden reasoning channel). Emitted only when > 0.
19
+ - `ModelCallOptions.reasoning.effort` mapped to the provider-native `reasoning_effort` request param. Forwarded only for reasoning-capable models; `reasoning.maxTokens` has no Chat Completions equivalent and is ignored.
20
+ - `ModelCapabilities.reasoning` inferred from the model name (`o1*` / `o3*` / `o4*` / `gpt-5*`), overridable via `.model({ name, reasoning })`. New `known-reasoning-models.ts` prefix helper.
21
+ - `ModelCapabilities.promptCaching` advertised as always `true` (OpenAI caches automatically and reports hits via `Usage.cachedTokens`). `ModelCallOptions.cacheControl` write breakpoints are a graceful no-op.
8
22
 
9
23
  ## 4.1.15
10
24
 
package/cjs/index.cjs CHANGED
@@ -404,6 +404,46 @@ var OpenAIEmbedder = class {
404
404
  }
405
405
  };
406
406
 
407
+ //#endregion
408
+ //#region ../@warlock.js/ai-openai/src/known-reasoning-models.ts
409
+ /**
410
+ * Model-name prefixes for OpenAI families that expose internal
411
+ * reasoning / thinking tokens and accept the `reasoning_effort`
412
+ * request parameter on the Chat Completions API.
413
+ *
414
+ * Matched as a prefix so dated variants (`o3-2025-04-16`) and
415
+ * `-mini` / `-pro` suffixes (`o4-mini`, `gpt-5-pro`) are covered
416
+ * without listing every release tag explicitly.
417
+ *
418
+ * Maintenance: append a new prefix when OpenAI ships a reasoning
419
+ * model family that doesn't already match. Devs can always override
420
+ * per-model via `openai.model({ name, reasoning: true | false })` —
421
+ * explicit config wins over inference in either direction.
422
+ */
423
+ const REASONING_CAPABLE_PREFIXES = [
424
+ "o1",
425
+ "o3",
426
+ "o4",
427
+ "gpt-5"
428
+ ];
429
+ /**
430
+ * Infer whether a given OpenAI model name is a reasoning model (o-series
431
+ * and the gpt-5 family) based on the known-prefix list. Unknown models
432
+ * default to `false` so the adapter never forwards an unsupported
433
+ * `reasoning_effort` param to a non-reasoning model (which would 400).
434
+ *
435
+ * @example
436
+ * inferReasoningCapability("o3-mini"); // → true
437
+ * inferReasoningCapability("o4-mini"); // → true
438
+ * inferReasoningCapability("gpt-5-pro"); // → true
439
+ * inferReasoningCapability("gpt-4o"); // → false
440
+ * inferReasoningCapability("custom-llm"); // → false
441
+ */
442
+ function inferReasoningCapability(modelName) {
443
+ const normalized = modelName.toLowerCase();
444
+ return REASONING_CAPABLE_PREFIXES.some((prefix) => normalized.startsWith(prefix));
445
+ }
446
+
407
447
  //#endregion
408
448
  //#region ../@warlock.js/ai-openai/src/known-vision-models.ts
409
449
  /**
@@ -507,7 +547,9 @@ var OpenAIModel = class {
507
547
  this.pricing = config.pricing;
508
548
  this.capabilities = {
509
549
  structuredOutput: config.structuredOutput ?? inferStructuredOutput(config.responseFormat),
510
- vision: config.vision ?? inferVisionCapability(config.name)
550
+ vision: config.vision ?? inferVisionCapability(config.name),
551
+ reasoning: config.reasoning ?? inferReasoningCapability(config.name),
552
+ promptCaching: true
511
553
  };
512
554
  }
513
555
  /**
@@ -531,7 +573,8 @@ var OpenAIModel = class {
531
573
  temperature: options?.temperature ?? this.config.temperature,
532
574
  max_tokens: options?.maxTokens ?? this.config.maxTokens,
533
575
  tools: toOpenAITools(options?.tools),
534
- ...this.buildResponseFormat(options?.responseSchema)
576
+ ...this.buildResponseFormat(options?.responseSchema),
577
+ ...this.buildReasoningParams(options?.reasoning)
535
578
  }, options?.signal ? { signal: options.signal } : void 0);
536
579
  } catch (thrown) {
537
580
  const wrapped = wrapOpenAIError(thrown);
@@ -578,7 +621,8 @@ var OpenAIModel = class {
578
621
  tools: toOpenAITools(options?.tools),
579
622
  stream: true,
580
623
  stream_options: { include_usage: true },
581
- ...this.buildResponseFormat(options?.responseSchema)
624
+ ...this.buildResponseFormat(options?.responseSchema),
625
+ ...this.buildReasoningParams(options?.reasoning)
582
626
  }, options?.signal ? { signal: options.signal } : void 0);
583
627
  } catch (thrown) {
584
628
  const wrapped = wrapOpenAIError(thrown);
@@ -622,6 +666,8 @@ var OpenAIModel = class {
622
666
  usage.total = chunk.usage.total_tokens ?? 0;
623
667
  const cached = chunk.usage.prompt_tokens_details?.cached_tokens;
624
668
  if (cached !== void 0 && cached > 0) usage.cachedTokens = cached;
669
+ const reasoning = chunk.usage.completion_tokens_details?.reasoning_tokens;
670
+ if (reasoning !== void 0 && reasoning > 0) usage.reasoningTokens = reasoning;
625
671
  }
626
672
  }
627
673
  for (const acc of toolCallAccum.values()) {
@@ -702,13 +748,53 @@ var OpenAIModel = class {
702
748
  * `json_object` mode is a safe degradation.
703
749
  */
704
750
  isStrictCompatible(schema) {
705
- return schema.type === "object" && typeof schema.properties === "object" && schema.properties !== null;
751
+ return schema.type === "object" && typeof schema.properties === "object" && schema.properties !== null && this.isStrictSafeNode(schema);
752
+ }
753
+ /**
754
+ * Recursively check the one strict-mode rule schemas most often trip on:
755
+ * every object must list ALL of its `properties` in `required` (OpenAI
756
+ * strict has no notion of optional — optional fields must be expressed
757
+ * as nullable, e.g. `type: ["string", "null"]`, and still appear in
758
+ * `required`). A schema that violates this anywhere in the tree is NOT
759
+ * sent in strict `json_schema` mode — it degrades to loose
760
+ * `json_object` so a hand-built or optional-bearing schema can't 400
761
+ * the call ("'required' ... must include every key in properties").
762
+ * Client-side `validate()` still enforces the full shape.
763
+ */
764
+ isStrictSafeNode(node) {
765
+ if (!node || typeof node !== "object") return true;
766
+ const record = node;
767
+ if (record.type === "object" && record.properties && typeof record.properties === "object") {
768
+ const properties = record.properties;
769
+ const keys = Object.keys(properties);
770
+ const required = Array.isArray(record.required) ? record.required : [];
771
+ if (keys.some((key) => !required.includes(key))) return false;
772
+ for (const key of keys) if (!this.isStrictSafeNode(properties[key])) return false;
773
+ }
774
+ if (record.items !== void 0 && !this.isStrictSafeNode(record.items)) return false;
775
+ for (const branch of [
776
+ "anyOf",
777
+ "allOf",
778
+ "oneOf"
779
+ ]) {
780
+ const value = record[branch];
781
+ if (Array.isArray(value) && value.some((sub) => !this.isStrictSafeNode(sub))) return false;
782
+ }
783
+ return true;
706
784
  }
707
785
  /**
708
786
  * Normalize OpenAI's `usage` block (which may be absent on some responses
709
787
  * or partials) into the neutral `Usage` shape. Missing usage collapses to
710
788
  * zeros rather than propagating `undefined`, so downstream aggregation
711
789
  * math stays safe.
790
+ *
791
+ * `cachedTokens` mirrors `prompt_tokens_details.cached_tokens` (the
792
+ * subset of the prompt served from OpenAI's automatic prompt cache);
793
+ * `reasoningTokens` mirrors `completion_tokens_details.reasoning_tokens`
794
+ * (the hidden reasoning channel on o-series / gpt-5 models, already
795
+ * counted within `output`). Both are emitted only when the provider
796
+ * reports a positive value, so non-reasoning / uncached calls keep the
797
+ * lean `{ input, output, total }` shape.
712
798
  */
713
799
  extractUsage(raw) {
714
800
  if (!raw) return {
@@ -717,14 +803,37 @@ var OpenAIModel = class {
717
803
  total: 0
718
804
  };
719
805
  const cachedTokens = raw.prompt_tokens_details?.cached_tokens;
806
+ const reasoningTokens = raw.completion_tokens_details?.reasoning_tokens;
720
807
  return {
721
808
  input: raw.prompt_tokens,
722
809
  output: raw.completion_tokens,
723
810
  total: raw.total_tokens,
724
- ...cachedTokens !== void 0 && cachedTokens > 0 ? { cachedTokens } : {}
811
+ ...cachedTokens !== void 0 && cachedTokens > 0 ? { cachedTokens } : {},
812
+ ...reasoningTokens !== void 0 && reasoningTokens > 0 ? { reasoningTokens } : {}
725
813
  };
726
814
  }
727
815
  /**
816
+ * Translate the neutral `ModelCallOptions.reasoning` hint into OpenAI's
817
+ * `reasoning_effort` request param. Only `effort` maps — OpenAI's Chat
818
+ * Completions API exposes a discrete effort knob, not a token budget,
819
+ * so `reasoning.maxTokens` (the Anthropic extended-thinking cap) has no
820
+ * wire equivalent here and is silently ignored.
821
+ *
822
+ * No-ops in two cases so the adapter never forwards an unsupported
823
+ * param: (1) the model is not reasoning-capable
824
+ * (`capabilities.reasoning` is false — e.g. `gpt-4o`), or (2) the caller
825
+ * supplied no `effort`. The neutral `ReasoningEffort`
826
+ * (`"low" | "medium" | "high"`) is a strict subset of OpenAI's accepted
827
+ * values, so it forwards verbatim.
828
+ *
829
+ * Returns an empty spread when nothing applies, so the caller can
830
+ * unconditionally `...buildReasoningParams(...)` into the request.
831
+ */
832
+ buildReasoningParams(reasoning) {
833
+ if (!this.capabilities.reasoning || !reasoning?.effort) return {};
834
+ return { reasoning_effort: reasoning.effort };
835
+ }
836
+ /**
728
837
  * Reshape OpenAI's `tool_calls` array into the neutral
729
838
  * `ModelToolCallRequest[]`. The raw `arguments` field is a JSON string
730
839
  * per OpenAI's protocol — we parse it defensively via `safeJsonParse` so
package/cjs/index.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["AIError","ProviderTimeoutError","ProviderAuthError","QuotaExceededError","ProviderRateLimitError","ContextLengthExceededError","ContentFilterError","InvalidRequestError","ProviderError","OpenAI","LOG_MODULE","log","log","OpenAI"],"sources":["../../../../../../@warlock.js/ai-openai/src/utils/map-finish-reason.ts","../../../../../../@warlock.js/ai-openai/src/utils/to-openai-messages.ts","../../../../../../@warlock.js/ai-openai/src/utils/to-openai-tools.ts","../../../../../../@warlock.js/ai-openai/src/utils/wrap-openai-error.ts","../../../../../../@warlock.js/ai-openai/src/embedder.ts","../../../../../../@warlock.js/ai-openai/src/known-vision-models.ts","../../../../../../@warlock.js/ai-openai/src/model.ts","../../../../../../@warlock.js/ai-openai/src/sdk.ts"],"sourcesContent":["import type { FinishReason } from \"@warlock.js/ai\";\n\nconst finishReasonMap: Record<string, FinishReason> = {\n stop: \"stop\",\n tool_calls: \"tool_calls\",\n length: \"length\",\n};\n\n/**\n * Map the raw OpenAI `finish_reason` string to the normalized FinishReason union.\n * Unknown/unexpected values fall through to \"error\".\n *\n * @example\n * mapFinishReason(\"stop\"); // \"stop\"\n * mapFinishReason(\"tool_calls\"); // \"tool_calls\"\n * mapFinishReason(null); // \"error\"\n */\nexport function mapFinishReason(raw: string | null | undefined): FinishReason {\n return finishReasonMap[raw ?? \"\"] ?? \"error\";\n}\n","import type { ContentPart, 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): OpenAI.Chat.Completions.ChatCompletionMessageParam[] {\n return 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 };\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 };\n }\n\n if (m.role === \"user\" && Array.isArray(m.content)) {\n return {\n role: \"user\",\n content: m.content.map(toOpenAIContentPart),\n };\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\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\nfunction toOpenAIContentPart(part: ContentPart): OpenAI.Chat.Completions.ChatCompletionContentPart {\n if (part.type === \"text\") {\n return { type: \"text\", text: part.text };\n }\n\n // TODO: Allow other types for urls not just images\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","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","import {\n AIError,\n ContentFilterError,\n ContextLengthExceededError,\n InvalidRequestError,\n ProviderAuthError,\n ProviderError,\n ProviderRateLimitError,\n ProviderTimeoutError,\n QuotaExceededError,\n} from \"@warlock.js/ai\";\nimport OpenAI from \"openai\";\n\n/**\n * Raw-error fields the wrapper reads off an OpenAI SDK error.\n *\n * `APIError` exposes `status`, `code`, `message`, `type`, `headers` —\n * we duck-type because wrapped retries, proxied errors, and custom\n * error subclasses sometimes lose the `instanceof` relationship.\n */\ntype OpenAIErrorShape = {\n status?: number;\n code?: string | null;\n message?: string;\n type?: string | null;\n headers?: Record<string, string> | undefined;\n name?: string;\n};\n\n/**\n * Wrap any thrown value caught inside the OpenAI adapter into the\n * appropriate `@warlock.js/ai` `AIError` subclass.\n *\n * **Dispatch strategy.** Prefers `APIError.code` when present (stable\n * machine identifier across SDK versions), falls back to `status` when\n * `code` is missing (common with proxied deployments that strip the\n * field). Name-based detection (`APIConnectionTimeoutError`) catches\n * transport-layer errors that never produced an HTTP response.\n *\n * `AIError` instances are returned unchanged — callers can pass the\n * error through `try/catch/throw wrap(e)` pipelines without accidental\n * double-wrapping.\n *\n * @example\n * try {\n * return await this.client.chat.completions.create(...);\n * } catch (thrown) {\n * throw wrapOpenAIError(thrown);\n * }\n */\nexport function wrapOpenAIError(thrown: unknown): AIError {\n if (thrown instanceof AIError) {\n return thrown;\n }\n\n const shape = toShape(thrown);\n const context = buildContext(thrown, shape);\n const message = shape.message ?? (thrown instanceof Error ? thrown.message : String(thrown));\n\n if (isTimeout(thrown, shape)) {\n return new ProviderTimeoutError(message, { cause: thrown, context });\n }\n\n if (shape.status === 401 || shape.code === \"invalid_api_key\") {\n return new ProviderAuthError(message, { cause: thrown, context });\n }\n\n if (shape.code === \"insufficient_quota\") {\n return new QuotaExceededError(message, { cause: thrown, context });\n }\n\n if (shape.status === 429 || shape.code === \"rate_limit_exceeded\") {\n return new ProviderRateLimitError(message, {\n cause: thrown,\n context,\n retryAfter: parseRetryAfter(shape.headers),\n });\n }\n\n if (shape.code === \"context_length_exceeded\") {\n return new ContextLengthExceededError(message, { cause: thrown, context });\n }\n\n if (shape.code === \"content_filter\") {\n return new ContentFilterError(message, {\n cause: thrown,\n context,\n reason: message,\n });\n }\n\n if (typeof shape.status === \"number\" && shape.status >= 400 && shape.status < 500) {\n return new InvalidRequestError(message, { cause: thrown, context });\n }\n\n return new ProviderError(message, { cause: thrown, context });\n}\n\n/**\n * Read the raw error shape without depending on `instanceof APIError`\n * — some consumers wrap the SDK, and proxies sometimes strip the\n * prototype chain. Duck-typing on the visible fields is resilient to\n * both.\n */\nfunction toShape(thrown: unknown): OpenAIErrorShape {\n if (thrown instanceof OpenAI.APIError) {\n return {\n status: thrown.status,\n code: thrown.code,\n message: thrown.message,\n type: thrown.type,\n headers: thrown.headers as Record<string, string> | undefined,\n name: thrown.name,\n };\n }\n\n if (typeof thrown === \"object\" && thrown !== null) {\n const raw = thrown as Record<string, unknown>;\n\n return {\n status: typeof raw.status === \"number\" ? raw.status : undefined,\n code: typeof raw.code === \"string\" ? raw.code : undefined,\n message: typeof raw.message === \"string\" ? raw.message : undefined,\n type: typeof raw.type === \"string\" ? raw.type : undefined,\n headers:\n typeof raw.headers === \"object\" && raw.headers !== null\n ? (raw.headers as Record<string, string>)\n : undefined,\n name: typeof raw.name === \"string\" ? raw.name : undefined,\n };\n }\n\n return {};\n}\n\n/**\n * Decide whether the thrown value represents a timeout. OpenAI's SDK\n * throws `APIConnectionTimeoutError` for transport-level timeouts, and\n * Node surfaces `ETIMEDOUT` / `ECONNABORTED` on the lower socket\n * layer. Either signal counts.\n */\nfunction isTimeout(thrown: unknown, shape: OpenAIErrorShape): boolean {\n if (thrown instanceof OpenAI.APIConnectionTimeoutError) {\n return true;\n }\n\n if (shape.name === \"APIConnectionTimeoutError\") {\n return true;\n }\n\n if (shape.code === \"ETIMEDOUT\" || shape.code === \"ECONNABORTED\") {\n return true;\n }\n\n return false;\n}\n\n/**\n * Attach the raw diagnostic fields to `error.context` so consumers\n * have everything the provider surfaced without each subclass having\n * to redeclare them. Never includes `cause` — that lives on\n * `error.cause`.\n */\nfunction buildContext(\n thrown: unknown,\n shape: OpenAIErrorShape,\n): Record<string, unknown> {\n const context: Record<string, unknown> = {};\n\n if (shape.status !== undefined) {\n context.status = shape.status;\n }\n\n if (shape.code) {\n context.code = shape.code;\n }\n\n if (shape.type) {\n context.type = shape.type;\n }\n\n const requestId = readRequestId(thrown);\n\n if (requestId) {\n context.requestId = requestId;\n }\n\n return context;\n}\n\n/**\n * OpenAI puts the request id on `APIError.request_id`. Extract\n * defensively — both camel and snake keys exist across SDK versions.\n */\nfunction readRequestId(thrown: unknown): string | undefined {\n if (typeof thrown !== \"object\" || thrown === null) {\n return undefined;\n }\n\n const raw = thrown as Record<string, unknown>;\n\n if (typeof raw.request_id === \"string\") {\n return raw.request_id;\n }\n\n if (typeof raw.requestId === \"string\") {\n return raw.requestId;\n }\n\n return undefined;\n}\n\n/**\n * Parse the `Retry-After` response header (seconds per HTTP spec)\n * into milliseconds so consumers can feed it straight to `setTimeout`.\n * Returns `undefined` when missing or unparseable.\n */\nfunction parseRetryAfter(headers: Record<string, string> | undefined): number | undefined {\n if (!headers) {\n return undefined;\n }\n\n const raw = headers[\"retry-after\"] ?? headers[\"Retry-After\"];\n\n if (!raw) {\n return undefined;\n }\n\n const seconds = Number(raw);\n\n if (!Number.isFinite(seconds) || seconds < 0) {\n return undefined;\n }\n\n return Math.round(seconds * 1000);\n}\n","import {\n type EmbeddingBatchResult,\n type EmbeddingResult,\n type EmbeddingUsage,\n type EmbedderContract,\n} from \"@warlock.js/ai\";\nimport { log, type Logger } from \"@warlock.js/logger\";\nimport type OpenAI from \"openai\";\nimport type { OpenAIEmbedderConfig } from \"./config.type\";\nimport { wrapOpenAIError } from \"./utils\";\n\nconst LOG_MODULE = \"ai.openai\";\n\ntype EmbeddingsResponse = Awaited<\n ReturnType<OpenAI[\"embeddings\"][\"create\"]>\n>;\n\n/**\n * OpenAI-backed implementation of `EmbedderContract`.\n *\n * **Role.** Converts text (or a batch of texts) into floating-point\n * vectors via OpenAI's Embeddings API. Standalone primitive — no\n * relationship to chat completions, tools, or the agent loop.\n *\n * **Dimensions.** When no `dimensions` override is supplied in config,\n * `this.dimensions` starts at `0` and is populated from the first\n * response's vector length, then cached for all subsequent calls —\n * even if a later response were to return a different length, the\n * first value wins so batches stay dimensionally consistent. Passing\n * `dimensions` in config both forwards the truncation hint to the API\n * (for models like `text-embedding-3-*`) and sets the initial value.\n *\n * **Error handling.** Raw OpenAI SDK errors are wrapped into the\n * typed `@warlock.js/ai` `AIError` hierarchy via `wrapOpenAIError` —\n * callers catch `AIError` subclasses (`ProviderRateLimitError`,\n * `ProviderAuthError`, etc.) instead of OpenAI's own classes.\n *\n * @example\n * const embedder = new OpenAIEmbedder(client, { name: \"text-embedding-3-small\" });\n * const { vector, dimensions, usage } = await embedder.embed(\"Hello world\");\n * const { vectors } = await embedder.embedMany([\"doc 1\", \"doc 2\"]);\n */\nexport class OpenAIEmbedder implements EmbedderContract {\n public readonly name: string;\n public readonly provider = \"openai\";\n public dimensions: number;\n\n private readonly client: OpenAI;\n\n /**\n * User-specified truncation hint, or `undefined` if omitted.\n * Forwarded to the API on every call so OpenAI can truncate the\n * embedding server-side for models that support it.\n */\n private readonly configuredDimensions: number | undefined;\n private readonly logger: Logger = log;\n\n public constructor(client: OpenAI, config: OpenAIEmbedderConfig) {\n this.client = client;\n this.name = config.name;\n this.configuredDimensions = config.dimensions;\n this.dimensions = config.dimensions ?? 0;\n }\n\n public async embed(input: string): Promise<EmbeddingResult> {\n const { response, usage } = await this.request(input);\n\n return {\n vector: response.data[0].embedding,\n dimensions: this.dimensions,\n usage,\n };\n }\n\n public async embedMany(inputs: string[]): Promise<EmbeddingBatchResult> {\n const { response, usage } = await this.request(inputs);\n\n return {\n vectors: response.data.map((d) => d.embedding),\n dimensions: this.dimensions,\n usage,\n };\n }\n\n /**\n * Shared transport for both `embed()` and `embedMany()` — issues the\n * `embeddings.create` call, wraps provider errors, caches dimensions\n * on the first successful response, and returns the raw response\n * plus a camelCase usage object for the caller to shape.\n */\n private async request(input: string | string[]): Promise<{\n response: EmbeddingsResponse;\n usage: EmbeddingUsage;\n }> {\n this.logger.debug(LOG_MODULE, \"embedder.request\", \"embeddings.create\", {\n model: this.name,\n batch: Array.isArray(input),\n count: Array.isArray(input) ? input.length : 1,\n });\n\n let response: EmbeddingsResponse;\n\n try {\n response = await this.client.embeddings.create({\n model: this.name,\n input,\n ...(this.configuredDimensions !== undefined\n ? { dimensions: this.configuredDimensions }\n : {}),\n });\n } catch (thrown) {\n const wrapped = wrapOpenAIError(thrown);\n\n this.logger.error(LOG_MODULE, \"embedder.error\", wrapped.message, {\n code: wrapped.code,\n context: wrapped.context,\n });\n\n throw wrapped;\n }\n\n this.logger.debug(LOG_MODULE, \"embedder.response\", \"embeddings.create returned\", {\n dimensions: response.data[0]?.embedding.length,\n usage: {\n promptTokens: response.usage.prompt_tokens,\n totalTokens: response.usage.total_tokens,\n },\n });\n\n // Cache dimensions on the first response. Once set, stays set —\n // we trust the first call to define the shape for this embedder.\n if (this.dimensions === 0) {\n this.dimensions = response.data[0].embedding.length;\n }\n\n const usage: EmbeddingUsage = {\n promptTokens: response.usage.prompt_tokens,\n totalTokens: response.usage.total_tokens,\n };\n\n return { response, usage };\n }\n}\n","/**\n * Model-name prefixes for OpenAI families that support vision input\n * (image attachments) on the Chat Completions API.\n *\n * Matched as a prefix so dated variants (`gpt-4o-2024-08-06`) and\n * `-mini` / `-preview` suffixes (`gpt-4o-mini`, `gpt-4-turbo-preview`)\n * are covered without listing every release tag explicitly.\n *\n * Maintenance: append a new prefix when OpenAI ships a vision-capable\n * model family that doesn't already match. Devs can always override\n * per-model via `openai.model({ name, vision: true | false })` —\n * explicit config wins over inference in either direction.\n */\nconst VISION_CAPABLE_PREFIXES = [\n \"gpt-4o\",\n \"gpt-4-turbo\",\n \"gpt-4.1\",\n \"o1\",\n \"o3\",\n \"chatgpt-4o\",\n];\n\n/**\n * Infer whether a given OpenAI model name supports vision based on the\n * known-prefix list. Unknown models default to `false` so that passing\n * an image attachment to an unsupported model surfaces a clear,\n * agent-side capability error instead of an opaque OpenAI 400.\n *\n * @example\n * inferVisionCapability(\"gpt-4o-mini\"); // → true\n * inferVisionCapability(\"gpt-4o-2024-08-06\"); // → true\n * inferVisionCapability(\"gpt-3.5-turbo\"); // → false\n * inferVisionCapability(\"custom-llm\"); // → false\n */\nexport function inferVisionCapability(modelName: string): boolean {\n const normalized = modelName.toLowerCase();\n\n return VISION_CAPABLE_PREFIXES.some((prefix) => normalized.startsWith(prefix));\n}\n","import {\n safeJsonParse,\n type Message,\n type ModelCallOptions,\n type ModelCapabilities,\n type ModelContract,\n type ModelPricing,\n type ModelResponse,\n type ModelStreamChunk,\n type ModelToolCallRequest,\n type Usage,\n} from \"@warlock.js/ai\";\nimport { log, type Logger } from \"@warlock.js/logger\";\nimport type OpenAI from \"openai\";\nimport type { OpenAIModelConfig, OpenAIResponseFormat } from \"./config.type\";\nimport { inferVisionCapability } from \"./known-vision-models\";\nimport { mapFinishReason, toOpenAIMessages, toOpenAITools, wrapOpenAIError } from \"./utils\";\n\nconst LOG_MODULE = \"ai.openai\";\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 */\nfunction inferStructuredOutput(responseFormat: OpenAIResponseFormat | undefined): boolean {\n if (responseFormat === \"json_object\" || responseFormat === \"text\") {\n return false;\n }\n\n return true;\n}\n\n/**\n * OpenAI-backed implementation of `ModelContract`.\n *\n * **Role.** The provider-facing bridge between the vendor-neutral\n * `@warlock.js/ai` agent runtime and the official `openai` SDK. Agents,\n * workflows, and supervisors never talk to OpenAI directly — they hold a\n * `ModelContract`, and this class is what makes that contract concrete for\n * any OpenAI-compatible endpoint (OpenAI, Azure OpenAI, OpenRouter, local\n * gateways that speak the Chat Completions protocol).\n *\n * **Responsibility.**\n * - Owns: a long-lived `OpenAI` client + frozen `ModelConfig` (name,\n * temperature, maxTokens) used as defaults for every call.\n * - Owns: translating vendor-neutral `Message[]` and\n * `ToolContract[]` into OpenAI wire shapes on the way out, and\n * translating OpenAI's response (content, finish reason, tool calls,\n * usage) back into the neutral shapes on the way in.\n * - Does NOT own: dispatching tools, deciding whether to loop, tracking\n * conversation history, or retrying on failure — those are agent\n * concerns. The model is a stateless (per-call) protocol adapter.\n *\n * Because it holds a live client and shared defaults, it is modeled as a\n * class (see §4.2 of code-style.md — \"long-lived state across calls\").\n *\n * @example\n * import OpenAI from \"openai\";\n * const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });\n * const model = new OpenAIModel(client, { name: \"gpt-4o\", temperature: 0.3 });\n *\n * const myAgent = agent({\n * model,\n * systemPrompt: \"You are a helpful assistant.\",\n * tools: [searchTool],\n * });\n *\n * const result = await myAgent.execute(\"Summarize today's news.\");\n */\nexport class OpenAIModel implements ModelContract {\n public readonly name: string;\n public readonly provider: string;\n public readonly capabilities: ModelCapabilities;\n public readonly pricing?: ModelPricing;\n\n private readonly client: OpenAI;\n private readonly config: OpenAIModelConfig;\n private readonly logger: Logger = log;\n\n public constructor(client: OpenAI, config: OpenAIModelConfig, provider: string = \"openai\") {\n this.client = client;\n this.config = config;\n this.name = config.name;\n this.provider = provider;\n this.pricing = config.pricing;\n this.capabilities = {\n structuredOutput: config.structuredOutput ?? inferStructuredOutput(config.responseFormat),\n vision: config.vision ?? inferVisionCapability(config.name),\n };\n }\n\n /**\n * Single-shot completion. Sends the full message list to the Chat\n * Completions endpoint, waits for the terminal response, and reshapes it\n * into a vendor-neutral `ModelResponse`. Per-call `options` override the\n * instance's `ModelConfig` defaults for this call only.\n */\n public async complete(messages: Message[], options?: ModelCallOptions): Promise<ModelResponse> {\n // Per-call request/response logs are hot-path in production agents\n // — keep them at `debug` so `info` stays reserved for lifecycle\n // events (agent starting/completed, etc.). Operators who need to\n // audit every LLM call can raise log-level at runtime.\n this.logger.debug(LOG_MODULE, \"request\", \"Starting call to chat.completions\", {\n model: this.name,\n messageCount: messages.length,\n streaming: false,\n toolCount: options?.tools?.length ?? 0,\n });\n\n let response: OpenAI.Chat.Completions.ChatCompletion;\n\n try {\n response = await this.client.chat.completions.create(\n {\n model: this.name,\n messages: toOpenAIMessages(messages),\n temperature: options?.temperature ?? this.config.temperature,\n max_tokens: options?.maxTokens ?? this.config.maxTokens,\n tools: toOpenAITools(options?.tools),\n ...this.buildResponseFormat(options?.responseSchema),\n },\n options?.signal ? { signal: options.signal } : undefined,\n );\n } catch (thrown) {\n const wrapped = wrapOpenAIError(thrown);\n\n this.logger.error(LOG_MODULE, \"error\", wrapped.message, {\n code: wrapped.code,\n context: wrapped.context,\n });\n\n throw wrapped;\n }\n\n const choice = response.choices[0];\n const finishReason = mapFinishReason(choice.finish_reason);\n const usage = this.extractUsage(response.usage);\n\n this.logger.debug(LOG_MODULE, \"response\", \"call to chat.completions succeeded\", {\n finishReason,\n usage,\n });\n\n return {\n content: choice.message.content ?? \"\",\n finishReason,\n usage,\n toolCalls: this.extractToolCalls(choice.message.tool_calls),\n };\n }\n\n /**\n * Incremental streaming completion. Yields neutral `ModelStreamChunk`s —\n * `delta` for text tokens, `tool-call` when the model requests a tool,\n * and a terminal `done` carrying the final finish reason + usage totals.\n * Callers consume it with `for await`.\n */\n public async *stream(\n messages: Message[],\n options?: ModelCallOptions,\n ): AsyncIterable<ModelStreamChunk> {\n this.logger.debug(LOG_MODULE, \"request\", \"Starting streaming call to chat.completions\", {\n model: this.name,\n messageCount: messages.length,\n streaming: true,\n toolCount: options?.tools?.length ?? 0,\n });\n\n let stream: Awaited<ReturnType<typeof this.client.chat.completions.create>>;\n\n try {\n stream = await this.client.chat.completions.create(\n {\n model: this.name,\n messages: toOpenAIMessages(messages),\n temperature: options?.temperature ?? this.config.temperature,\n max_tokens: options?.maxTokens ?? this.config.maxTokens,\n tools: toOpenAITools(options?.tools),\n stream: true,\n stream_options: { include_usage: true },\n ...this.buildResponseFormat(options?.responseSchema),\n },\n options?.signal ? { signal: options.signal } : undefined,\n );\n } catch (thrown) {\n const wrapped = wrapOpenAIError(thrown);\n\n this.logger.error(LOG_MODULE, \"error\", wrapped.message, {\n code: wrapped.code,\n context: wrapped.context,\n });\n\n throw wrapped;\n }\n\n let rawFinishReason: string = \"stop\";\n const usage: Usage = { input: 0, output: 0, total: 0 };\n const toolCallAccum = new Map<number, { id: string; name: string; arguments: string }>();\n\n try {\n for await (const chunk of stream as AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>) {\n const delta = chunk.choices[0]?.delta;\n const finish = chunk.choices[0]?.finish_reason;\n\n if (delta?.content) {\n yield { type: \"delta\", content: delta.content };\n }\n\n if (delta?.tool_calls) {\n for (const toolCall of delta.tool_calls) {\n const idx = toolCall.index ?? 0;\n if (!toolCallAccum.has(idx)) {\n toolCallAccum.set(idx, { id: \"\", name: \"\", arguments: \"\" });\n }\n const acc = toolCallAccum.get(idx)!;\n if (toolCall.id) acc.id = toolCall.id;\n if (toolCall.function?.name) acc.name = toolCall.function.name;\n if (toolCall.function?.arguments) acc.arguments += toolCall.function.arguments;\n }\n }\n\n if (finish) {\n rawFinishReason = finish;\n }\n\n if (chunk.usage) {\n usage.input = chunk.usage.prompt_tokens ?? 0;\n usage.output = chunk.usage.completion_tokens ?? 0;\n usage.total = chunk.usage.total_tokens ?? 0;\n const cached = chunk.usage.prompt_tokens_details?.cached_tokens;\n if (cached !== undefined && cached > 0) {\n usage.cachedTokens = cached;\n }\n }\n }\n\n for (const acc of toolCallAccum.values()) {\n // Skip accumulators that never received a function name — those\n // are partial fragments the model started but never identified\n // (e.g. arguments-only deltas with no originating `id`/`name`).\n // Yielding them produces nameless tool-calls the agent runtime\n // can't dispatch and would mis-attribute as a registered tool.\n if (!acc.name) continue;\n\n yield {\n type: \"tool-call\",\n id: acc.id,\n name: acc.name,\n input: safeJsonParse<Record<string, unknown>>(acc.arguments, {}),\n };\n }\n } catch (thrown) {\n const wrapped = wrapOpenAIError(thrown);\n\n this.logger.error(LOG_MODULE, \"error\", wrapped.message, {\n code: wrapped.code,\n context: wrapped.context,\n });\n\n throw wrapped;\n }\n\n const finishReason = mapFinishReason(rawFinishReason);\n\n this.logger.debug(LOG_MODULE, \"response\", \"Streaming call to chat.completions succeeded\", {\n finishReason,\n usage,\n });\n\n yield { type: \"done\", finishReason, usage };\n }\n\n /**\n * Translate the neutral `responseSchema` option into OpenAI's\n * `response_format` parameter.\n *\n * When `config.responseFormat` is set, it wins: `\"text\"` emits no\n * `response_format` at all, `\"json_object\"` always picks the loose\n * mode, and `\"json_schema\"` picks strict mode (with the same\n * `isStrictCompatible` safety check — a malformed schema still\n * degrades to `json_object` rather than 400). The override exists\n * because some targets (older OpenAI models, OpenRouter routes,\n * Ollama OpenAI-compat) reject strict `json_schema` outright.\n *\n * When the override is omitted, uses strict `json_schema` mode\n * (token-level enforcement) only when the schema is a proper\n * root-object JSON Schema (`{ type: \"object\", properties: ... }`).\n * For anything else — malformed extractor output, non-object\n * schemas, or future shapes we haven't tested — falls back to loose\n * `json_object` mode, which guarantees *some* valid JSON without\n * enforcing shape. The agent's soft instruction already embeds the\n * schema text in the system prompt when the model declares no\n * native structured-output capability, so shape validation still\n * runs client-side via the Standard Schema `validate()` call.\n *\n * Returns an empty spread when no schema was supplied, so the caller\n * can unconditionally `...buildResponseFormat(...)` into the request.\n */\n private buildResponseFormat(responseSchema: Record<string, unknown> | undefined): {\n response_format?: OpenAI.Chat.Completions.ChatCompletionCreateParams[\"response_format\"];\n } {\n if (!responseSchema) {\n return {};\n }\n\n const override = this.config.responseFormat;\n\n if (override === \"text\") {\n return {};\n }\n\n if (override === \"json_object\") {\n return { response_format: { type: \"json_object\" } };\n }\n\n // Either auto-select (no override) or explicit `\"json_schema\"`.\n // The strict-compat check still applies in the explicit case —\n // a malformed / non-object schema would 400 before sampling, so\n // we degrade to `json_object` rather than crash.\n if (this.isStrictCompatible(responseSchema)) {\n return {\n response_format: {\n type: \"json_schema\",\n json_schema: {\n name: \"response\",\n schema: responseSchema,\n strict: true,\n },\n },\n };\n }\n\n return { response_format: { type: \"json_object\" } };\n }\n\n /**\n * OpenAI strict `json_schema` mode requires the root to be a JSON\n * Schema object type (`{ type: \"object\", properties: ... }`). Anything\n * else (top-level arrays, primitives, unknown shapes) is rejected with\n * a 400 before a token is sampled. We check structurally here so the\n * first call doesn't crash on a malformed extraction — loose\n * `json_object` mode is a safe degradation.\n */\n private isStrictCompatible(schema: Record<string, unknown>): boolean {\n return (\n schema.type === \"object\" &&\n typeof schema.properties === \"object\" &&\n schema.properties !== null\n );\n }\n\n /**\n * Normalize OpenAI's `usage` block (which may be absent on some responses\n * or partials) into the neutral `Usage` shape. Missing usage collapses to\n * zeros rather than propagating `undefined`, so downstream aggregation\n * math stays safe.\n */\n private extractUsage(raw: OpenAI.Completions.CompletionUsage | undefined): Usage {\n if (!raw) {\n return { input: 0, output: 0, total: 0 };\n }\n\n const cachedTokens = raw.prompt_tokens_details?.cached_tokens;\n\n return {\n input: raw.prompt_tokens,\n output: raw.completion_tokens,\n total: raw.total_tokens,\n ...(cachedTokens !== undefined && cachedTokens > 0 ? { cachedTokens } : {}),\n };\n }\n\n /**\n * Reshape OpenAI's `tool_calls` array into the neutral\n * `ModelToolCallRequest[]`. The raw `arguments` field is a JSON string\n * per OpenAI's protocol — we parse it defensively via `safeJsonParse` so\n * malformed or empty arguments yield an empty object instead of crashing\n * the trip. Returns `undefined` when no tools were requested so callers\n * can branch on presence.\n */\n private extractToolCalls(\n rawToolCalls: OpenAI.Chat.Completions.ChatCompletionMessageToolCall[] | undefined,\n ): ModelToolCallRequest[] | undefined {\n if (!rawToolCalls || rawToolCalls.length === 0) {\n return undefined;\n }\n\n return rawToolCalls.map((toolCall) => ({\n id: toolCall.id,\n name: (toolCall as any).function.name,\n input: safeJsonParse<Record<string, unknown>>((toolCall as any).function.arguments, {}),\n }));\n }\n}\n","import OpenAI from \"openai\";\nimport type {\n EmbedderContract,\n ModelContract,\n ModelPricing,\n SDKAdapterContract,\n} from \"@warlock.js/ai\";\nimport { approximateTokenCount } from \"@warlock.js/ai\";\nimport type { OpenAIEmbedderConfig, OpenAIModelConfig, OpenAISDKConfig } from \"./config.type\";\nimport { OpenAIEmbedder } from \"./embedder\";\nimport { OpenAIModel } from \"./model\";\n\n/**\n * OpenAI-backed implementation of `SDKAdapterContract`.\n *\n * **Role.** The package entry point for any OpenAI-compatible provider\n * (OpenAI, Azure OpenAI, OpenRouter, local gateways speaking the Chat\n * Completions protocol). A single `OpenAISDK` instance holds one live\n * `OpenAI` client, shared by every `ModelContract` it produces via\n * `model()`. Users construct one SDK per provider/account and reuse it\n * across all agents, workflows, and supervisors that target that\n * provider.\n *\n * **Responsibility.**\n * - Owns: a long-lived `OpenAI` client (authentication, base URL) and\n * its lifetime scope. Factory for `OpenAIModel` instances — each\n * model call gets a reference to the same client.\n * - Does NOT own: anything per-call (tool execution, message history,\n * streaming loop) — those live in `OpenAIModel` and the agent runtime.\n *\n * Modeled as a class (see §4.2 of code-style.md — \"long-lived state\n * across many calls\"): the `OpenAI` client is heavy to construct and\n * designed to be reused; keeping it on `this` makes that reuse\n * explicit and aligns with the PascalCase naming convention readers\n * expect from a constructor.\n *\n * @example\n * const openai = new OpenAISDK({ apiKey: process.env.OPENAI_API_KEY! });\n * const model = openai.model({ name: \"gpt-4o\", temperature: 0.7 });\n * const tokens = await openai.count(\"Hello world\");\n *\n * @example\n * // Compose into an `ai.openai` namespace for ergonomic agent wiring\n * const ai = { agent, tool, systemPrompt, persona, instruction, openai: new OpenAISDK({ apiKey }) };\n * const myAgent = ai.agent({ model: ai.openai.model({ name: \"gpt-4o-mini\" }) });\n */\nexport class OpenAISDK implements SDKAdapterContract {\n private readonly client: OpenAI;\n private readonly provider: string;\n private readonly pricing?: Record<string, ModelPricing>;\n\n public constructor(config: OpenAISDKConfig) {\n this.client = new OpenAI({\n apiKey: config.apiKey,\n baseURL: config.baseURL,\n });\n this.provider = config.provider ?? \"openai\";\n this.pricing = config.pricing;\n }\n\n /**\n * Build an `OpenAIModel` bound to this SDK's client. Each call returns\n * a fresh model instance, but all instances share the underlying\n * `OpenAI` client — connection pools, rate limits, and authentication\n * state stay unified across every model produced here. The SDK's\n * `provider` label is forwarded so every model self-identifies as\n * coming from the same upstream.\n *\n * Pricing resolution: per-model `config.pricing` wins; otherwise the\n * SDK-level registry entry keyed by `config.name`; otherwise\n * `undefined` (no cost computed).\n */\n public model(config: OpenAIModelConfig): ModelContract {\n const resolvedPricing = config.pricing ?? this.pricing?.[config.name];\n const resolvedConfig: OpenAIModelConfig =\n resolvedPricing === config.pricing ? config : { ...config, pricing: resolvedPricing };\n\n return new OpenAIModel(this.client, resolvedConfig, this.provider);\n }\n\n /**\n * Rough token-count estimate for a given text. Uses a\n * character-heuristic (`approximateTokenCount`) from the core package\n * — good enough for budgeting and quota guards, not for billing.\n * Accepts an optional model id for future per-model tokenizer\n * dispatch; currently ignored.\n */\n public async count(text: string, _model?: string): Promise<number> {\n return approximateTokenCount(text);\n }\n\n /**\n * Build an `OpenAIEmbedder` bound to this SDK's client. Each call\n * returns a fresh embedder instance sharing the same underlying\n * `OpenAI` client — connection pools and authentication stay unified\n * across every embedder produced here.\n *\n * @example\n * const embedder = openai.embedder({ name: \"text-embedding-3-small\" });\n * const { vector } = await embedder.embed(\"Hello world\");\n */\n public embedder(config: OpenAIEmbedderConfig): EmbedderContract {\n return new OpenAIEmbedder(this.client, config);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["AIError","ProviderTimeoutError","ProviderAuthError","QuotaExceededError","ProviderRateLimitError","ContextLengthExceededError","ContentFilterError","InvalidRequestError","ProviderError","OpenAI","LOG_MODULE","log","log","OpenAI"],"sources":["../../../../../../@warlock.js/ai-openai/src/utils/map-finish-reason.ts","../../../../../../@warlock.js/ai-openai/src/utils/to-openai-messages.ts","../../../../../../@warlock.js/ai-openai/src/utils/to-openai-tools.ts","../../../../../../@warlock.js/ai-openai/src/utils/wrap-openai-error.ts","../../../../../../@warlock.js/ai-openai/src/embedder.ts","../../../../../../@warlock.js/ai-openai/src/known-reasoning-models.ts","../../../../../../@warlock.js/ai-openai/src/known-vision-models.ts","../../../../../../@warlock.js/ai-openai/src/model.ts","../../../../../../@warlock.js/ai-openai/src/sdk.ts"],"sourcesContent":["import type { FinishReason } from \"@warlock.js/ai\";\n\nconst finishReasonMap: Record<string, FinishReason> = {\n stop: \"stop\",\n tool_calls: \"tool_calls\",\n length: \"length\",\n};\n\n/**\n * Map the raw OpenAI `finish_reason` string to the normalized FinishReason union.\n * Unknown/unexpected values fall through to \"error\".\n *\n * @example\n * mapFinishReason(\"stop\"); // \"stop\"\n * mapFinishReason(\"tool_calls\"); // \"tool_calls\"\n * mapFinishReason(null); // \"error\"\n */\nexport function mapFinishReason(raw: string | null | undefined): FinishReason {\n return finishReasonMap[raw ?? \"\"] ?? \"error\";\n}\n","import type { ContentPart, 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): OpenAI.Chat.Completions.ChatCompletionMessageParam[] {\n return 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 };\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 };\n }\n\n if (m.role === \"user\" && Array.isArray(m.content)) {\n return {\n role: \"user\",\n content: m.content.map(toOpenAIContentPart),\n };\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\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\nfunction toOpenAIContentPart(part: ContentPart): OpenAI.Chat.Completions.ChatCompletionContentPart {\n if (part.type === \"text\") {\n return { type: \"text\", text: part.text };\n }\n\n // TODO: Allow other types for urls not just images\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","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","import {\n AIError,\n ContentFilterError,\n ContextLengthExceededError,\n InvalidRequestError,\n ProviderAuthError,\n ProviderError,\n ProviderRateLimitError,\n ProviderTimeoutError,\n QuotaExceededError,\n} from \"@warlock.js/ai\";\nimport OpenAI from \"openai\";\n\n/**\n * Raw-error fields the wrapper reads off an OpenAI SDK error.\n *\n * `APIError` exposes `status`, `code`, `message`, `type`, `headers` —\n * we duck-type because wrapped retries, proxied errors, and custom\n * error subclasses sometimes lose the `instanceof` relationship.\n */\ntype OpenAIErrorShape = {\n status?: number;\n code?: string | null;\n message?: string;\n type?: string | null;\n headers?: Record<string, string> | undefined;\n name?: string;\n};\n\n/**\n * Wrap any thrown value caught inside the OpenAI adapter into the\n * appropriate `@warlock.js/ai` `AIError` subclass.\n *\n * **Dispatch strategy.** Prefers `APIError.code` when present (stable\n * machine identifier across SDK versions), falls back to `status` when\n * `code` is missing (common with proxied deployments that strip the\n * field). Name-based detection (`APIConnectionTimeoutError`) catches\n * transport-layer errors that never produced an HTTP response.\n *\n * `AIError` instances are returned unchanged — callers can pass the\n * error through `try/catch/throw wrap(e)` pipelines without accidental\n * double-wrapping.\n *\n * @example\n * try {\n * return await this.client.chat.completions.create(...);\n * } catch (thrown) {\n * throw wrapOpenAIError(thrown);\n * }\n */\nexport function wrapOpenAIError(thrown: unknown): AIError {\n if (thrown instanceof AIError) {\n return thrown;\n }\n\n const shape = toShape(thrown);\n const context = buildContext(thrown, shape);\n const message = shape.message ?? (thrown instanceof Error ? thrown.message : String(thrown));\n\n if (isTimeout(thrown, shape)) {\n return new ProviderTimeoutError(message, { cause: thrown, context });\n }\n\n if (shape.status === 401 || shape.code === \"invalid_api_key\") {\n return new ProviderAuthError(message, { cause: thrown, context });\n }\n\n if (shape.code === \"insufficient_quota\") {\n return new QuotaExceededError(message, { cause: thrown, context });\n }\n\n if (shape.status === 429 || shape.code === \"rate_limit_exceeded\") {\n return new ProviderRateLimitError(message, {\n cause: thrown,\n context,\n retryAfter: parseRetryAfter(shape.headers),\n });\n }\n\n if (shape.code === \"context_length_exceeded\") {\n return new ContextLengthExceededError(message, { cause: thrown, context });\n }\n\n if (shape.code === \"content_filter\") {\n return new ContentFilterError(message, {\n cause: thrown,\n context,\n reason: message,\n });\n }\n\n if (typeof shape.status === \"number\" && shape.status >= 400 && shape.status < 500) {\n return new InvalidRequestError(message, { cause: thrown, context });\n }\n\n return new ProviderError(message, { cause: thrown, context });\n}\n\n/**\n * Read the raw error shape without depending on `instanceof APIError`\n * — some consumers wrap the SDK, and proxies sometimes strip the\n * prototype chain. Duck-typing on the visible fields is resilient to\n * both.\n */\nfunction toShape(thrown: unknown): OpenAIErrorShape {\n if (thrown instanceof OpenAI.APIError) {\n return {\n status: thrown.status,\n code: thrown.code,\n message: thrown.message,\n type: thrown.type,\n headers: thrown.headers as Record<string, string> | undefined,\n name: thrown.name,\n };\n }\n\n if (typeof thrown === \"object\" && thrown !== null) {\n const raw = thrown as Record<string, unknown>;\n\n return {\n status: typeof raw.status === \"number\" ? raw.status : undefined,\n code: typeof raw.code === \"string\" ? raw.code : undefined,\n message: typeof raw.message === \"string\" ? raw.message : undefined,\n type: typeof raw.type === \"string\" ? raw.type : undefined,\n headers:\n typeof raw.headers === \"object\" && raw.headers !== null\n ? (raw.headers as Record<string, string>)\n : undefined,\n name: typeof raw.name === \"string\" ? raw.name : undefined,\n };\n }\n\n return {};\n}\n\n/**\n * Decide whether the thrown value represents a timeout. OpenAI's SDK\n * throws `APIConnectionTimeoutError` for transport-level timeouts, and\n * Node surfaces `ETIMEDOUT` / `ECONNABORTED` on the lower socket\n * layer. Either signal counts.\n */\nfunction isTimeout(thrown: unknown, shape: OpenAIErrorShape): boolean {\n if (thrown instanceof OpenAI.APIConnectionTimeoutError) {\n return true;\n }\n\n if (shape.name === \"APIConnectionTimeoutError\") {\n return true;\n }\n\n if (shape.code === \"ETIMEDOUT\" || shape.code === \"ECONNABORTED\") {\n return true;\n }\n\n return false;\n}\n\n/**\n * Attach the raw diagnostic fields to `error.context` so consumers\n * have everything the provider surfaced without each subclass having\n * to redeclare them. Never includes `cause` — that lives on\n * `error.cause`.\n */\nfunction buildContext(\n thrown: unknown,\n shape: OpenAIErrorShape,\n): Record<string, unknown> {\n const context: Record<string, unknown> = {};\n\n if (shape.status !== undefined) {\n context.status = shape.status;\n }\n\n if (shape.code) {\n context.code = shape.code;\n }\n\n if (shape.type) {\n context.type = shape.type;\n }\n\n const requestId = readRequestId(thrown);\n\n if (requestId) {\n context.requestId = requestId;\n }\n\n return context;\n}\n\n/**\n * OpenAI puts the request id on `APIError.request_id`. Extract\n * defensively — both camel and snake keys exist across SDK versions.\n */\nfunction readRequestId(thrown: unknown): string | undefined {\n if (typeof thrown !== \"object\" || thrown === null) {\n return undefined;\n }\n\n const raw = thrown as Record<string, unknown>;\n\n if (typeof raw.request_id === \"string\") {\n return raw.request_id;\n }\n\n if (typeof raw.requestId === \"string\") {\n return raw.requestId;\n }\n\n return undefined;\n}\n\n/**\n * Parse the `Retry-After` response header (seconds per HTTP spec)\n * into milliseconds so consumers can feed it straight to `setTimeout`.\n * Returns `undefined` when missing or unparseable.\n */\nfunction parseRetryAfter(headers: Record<string, string> | undefined): number | undefined {\n if (!headers) {\n return undefined;\n }\n\n const raw = headers[\"retry-after\"] ?? headers[\"Retry-After\"];\n\n if (!raw) {\n return undefined;\n }\n\n const seconds = Number(raw);\n\n if (!Number.isFinite(seconds) || seconds < 0) {\n return undefined;\n }\n\n return Math.round(seconds * 1000);\n}\n","import {\n type EmbeddingBatchResult,\n type EmbeddingResult,\n type EmbeddingUsage,\n type EmbedderContract,\n} from \"@warlock.js/ai\";\nimport { log, type Logger } from \"@warlock.js/logger\";\nimport type OpenAI from \"openai\";\nimport type { OpenAIEmbedderConfig } from \"./config.type\";\nimport { wrapOpenAIError } from \"./utils\";\n\nconst LOG_MODULE = \"ai.openai\";\n\ntype EmbeddingsResponse = Awaited<\n ReturnType<OpenAI[\"embeddings\"][\"create\"]>\n>;\n\n/**\n * OpenAI-backed implementation of `EmbedderContract`.\n *\n * **Role.** Converts text (or a batch of texts) into floating-point\n * vectors via OpenAI's Embeddings API. Standalone primitive — no\n * relationship to chat completions, tools, or the agent loop.\n *\n * **Dimensions.** When no `dimensions` override is supplied in config,\n * `this.dimensions` starts at `0` and is populated from the first\n * response's vector length, then cached for all subsequent calls —\n * even if a later response were to return a different length, the\n * first value wins so batches stay dimensionally consistent. Passing\n * `dimensions` in config both forwards the truncation hint to the API\n * (for models like `text-embedding-3-*`) and sets the initial value.\n *\n * **Error handling.** Raw OpenAI SDK errors are wrapped into the\n * typed `@warlock.js/ai` `AIError` hierarchy via `wrapOpenAIError` —\n * callers catch `AIError` subclasses (`ProviderRateLimitError`,\n * `ProviderAuthError`, etc.) instead of OpenAI's own classes.\n *\n * @example\n * const embedder = new OpenAIEmbedder(client, { name: \"text-embedding-3-small\" });\n * const { vector, dimensions, usage } = await embedder.embed(\"Hello world\");\n * const { vectors } = await embedder.embedMany([\"doc 1\", \"doc 2\"]);\n */\nexport class OpenAIEmbedder implements EmbedderContract {\n public readonly name: string;\n public readonly provider = \"openai\";\n public dimensions: number;\n\n private readonly client: OpenAI;\n\n /**\n * User-specified truncation hint, or `undefined` if omitted.\n * Forwarded to the API on every call so OpenAI can truncate the\n * embedding server-side for models that support it.\n */\n private readonly configuredDimensions: number | undefined;\n private readonly logger: Logger = log;\n\n public constructor(client: OpenAI, config: OpenAIEmbedderConfig) {\n this.client = client;\n this.name = config.name;\n this.configuredDimensions = config.dimensions;\n this.dimensions = config.dimensions ?? 0;\n }\n\n public async embed(input: string): Promise<EmbeddingResult> {\n const { response, usage } = await this.request(input);\n\n return {\n vector: response.data[0].embedding,\n dimensions: this.dimensions,\n usage,\n };\n }\n\n public async embedMany(inputs: string[]): Promise<EmbeddingBatchResult> {\n const { response, usage } = await this.request(inputs);\n\n return {\n vectors: response.data.map((d) => d.embedding),\n dimensions: this.dimensions,\n usage,\n };\n }\n\n /**\n * Shared transport for both `embed()` and `embedMany()` — issues the\n * `embeddings.create` call, wraps provider errors, caches dimensions\n * on the first successful response, and returns the raw response\n * plus a camelCase usage object for the caller to shape.\n */\n private async request(input: string | string[]): Promise<{\n response: EmbeddingsResponse;\n usage: EmbeddingUsage;\n }> {\n this.logger.debug(LOG_MODULE, \"embedder.request\", \"embeddings.create\", {\n model: this.name,\n batch: Array.isArray(input),\n count: Array.isArray(input) ? input.length : 1,\n });\n\n let response: EmbeddingsResponse;\n\n try {\n response = await this.client.embeddings.create({\n model: this.name,\n input,\n ...(this.configuredDimensions !== undefined\n ? { dimensions: this.configuredDimensions }\n : {}),\n });\n } catch (thrown) {\n const wrapped = wrapOpenAIError(thrown);\n\n this.logger.error(LOG_MODULE, \"embedder.error\", wrapped.message, {\n code: wrapped.code,\n context: wrapped.context,\n });\n\n throw wrapped;\n }\n\n this.logger.debug(LOG_MODULE, \"embedder.response\", \"embeddings.create returned\", {\n dimensions: response.data[0]?.embedding.length,\n usage: {\n promptTokens: response.usage.prompt_tokens,\n totalTokens: response.usage.total_tokens,\n },\n });\n\n // Cache dimensions on the first response. Once set, stays set —\n // we trust the first call to define the shape for this embedder.\n if (this.dimensions === 0) {\n this.dimensions = response.data[0].embedding.length;\n }\n\n const usage: EmbeddingUsage = {\n promptTokens: response.usage.prompt_tokens,\n totalTokens: response.usage.total_tokens,\n };\n\n return { response, usage };\n }\n}\n","/**\n * Model-name prefixes for OpenAI families that expose internal\n * reasoning / thinking tokens and accept the `reasoning_effort`\n * request parameter on the Chat Completions API.\n *\n * Matched as a prefix so dated variants (`o3-2025-04-16`) and\n * `-mini` / `-pro` suffixes (`o4-mini`, `gpt-5-pro`) are covered\n * without listing every release tag explicitly.\n *\n * Maintenance: append a new prefix when OpenAI ships a reasoning\n * model family that doesn't already match. Devs can always override\n * per-model via `openai.model({ name, reasoning: true | false })` —\n * explicit config wins over inference in either direction.\n */\nconst REASONING_CAPABLE_PREFIXES = [\"o1\", \"o3\", \"o4\", \"gpt-5\"];\n\n/**\n * Infer whether a given OpenAI model name is a reasoning model (o-series\n * and the gpt-5 family) based on the known-prefix list. Unknown models\n * default to `false` so the adapter never forwards an unsupported\n * `reasoning_effort` param to a non-reasoning model (which would 400).\n *\n * @example\n * inferReasoningCapability(\"o3-mini\"); // → true\n * inferReasoningCapability(\"o4-mini\"); // → true\n * inferReasoningCapability(\"gpt-5-pro\"); // → true\n * inferReasoningCapability(\"gpt-4o\"); // → false\n * inferReasoningCapability(\"custom-llm\"); // → false\n */\nexport function inferReasoningCapability(modelName: string): boolean {\n const normalized = modelName.toLowerCase();\n\n return REASONING_CAPABLE_PREFIXES.some((prefix) => normalized.startsWith(prefix));\n}\n","/**\n * Model-name prefixes for OpenAI families that support vision input\n * (image attachments) on the Chat Completions API.\n *\n * Matched as a prefix so dated variants (`gpt-4o-2024-08-06`) and\n * `-mini` / `-preview` suffixes (`gpt-4o-mini`, `gpt-4-turbo-preview`)\n * are covered without listing every release tag explicitly.\n *\n * Maintenance: append a new prefix when OpenAI ships a vision-capable\n * model family that doesn't already match. Devs can always override\n * per-model via `openai.model({ name, vision: true | false })` —\n * explicit config wins over inference in either direction.\n */\nconst VISION_CAPABLE_PREFIXES = [\n \"gpt-4o\",\n \"gpt-4-turbo\",\n \"gpt-4.1\",\n \"o1\",\n \"o3\",\n \"chatgpt-4o\",\n];\n\n/**\n * Infer whether a given OpenAI model name supports vision based on the\n * known-prefix list. Unknown models default to `false` so that passing\n * an image attachment to an unsupported model surfaces a clear,\n * agent-side capability error instead of an opaque OpenAI 400.\n *\n * @example\n * inferVisionCapability(\"gpt-4o-mini\"); // → true\n * inferVisionCapability(\"gpt-4o-2024-08-06\"); // → true\n * inferVisionCapability(\"gpt-3.5-turbo\"); // → false\n * inferVisionCapability(\"custom-llm\"); // → false\n */\nexport function inferVisionCapability(modelName: string): boolean {\n const normalized = modelName.toLowerCase();\n\n return VISION_CAPABLE_PREFIXES.some((prefix) => normalized.startsWith(prefix));\n}\n","import {\n safeJsonParse,\n type Message,\n type ModelCallOptions,\n type ModelCapabilities,\n type ModelContract,\n type ModelPricing,\n type ModelResponse,\n type ModelStreamChunk,\n type ModelToolCallRequest,\n type Usage,\n} from \"@warlock.js/ai\";\nimport { log, type Logger } from \"@warlock.js/logger\";\nimport type OpenAI from \"openai\";\nimport type { OpenAIModelConfig, OpenAIResponseFormat } from \"./config.type\";\nimport { inferReasoningCapability } from \"./known-reasoning-models\";\nimport { inferVisionCapability } from \"./known-vision-models\";\nimport { mapFinishReason, toOpenAIMessages, toOpenAITools, wrapOpenAIError } from \"./utils\";\n\nconst LOG_MODULE = \"ai.openai\";\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 */\nfunction inferStructuredOutput(responseFormat: OpenAIResponseFormat | undefined): boolean {\n if (responseFormat === \"json_object\" || responseFormat === \"text\") {\n return false;\n }\n\n return true;\n}\n\n/**\n * OpenAI-backed implementation of `ModelContract`.\n *\n * **Role.** The provider-facing bridge between the vendor-neutral\n * `@warlock.js/ai` agent runtime and the official `openai` SDK. Agents,\n * workflows, and supervisors never talk to OpenAI directly — they hold a\n * `ModelContract`, and this class is what makes that contract concrete for\n * any OpenAI-compatible endpoint (OpenAI, Azure OpenAI, OpenRouter, local\n * gateways that speak the Chat Completions protocol).\n *\n * **Responsibility.**\n * - Owns: a long-lived `OpenAI` client + frozen `ModelConfig` (name,\n * temperature, maxTokens) used as defaults for every call.\n * - Owns: translating vendor-neutral `Message[]` and\n * `ToolContract[]` into OpenAI wire shapes on the way out, and\n * translating OpenAI's response (content, finish reason, tool calls,\n * usage) back into the neutral shapes on the way in.\n * - Does NOT own: dispatching tools, deciding whether to loop, tracking\n * conversation history, or retrying on failure — those are agent\n * concerns. The model is a stateless (per-call) protocol adapter.\n *\n * Because it holds a live client and shared defaults, it is modeled as a\n * class (see §4.2 of code-style.md — \"long-lived state across calls\").\n *\n * @example\n * import OpenAI from \"openai\";\n * const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });\n * const model = new OpenAIModel(client, { name: \"gpt-4o\", temperature: 0.3 });\n *\n * const myAgent = agent({\n * model,\n * systemPrompt: \"You are a helpful assistant.\",\n * tools: [searchTool],\n * });\n *\n * const result = await myAgent.execute(\"Summarize today's news.\");\n */\nexport class OpenAIModel implements ModelContract {\n public readonly name: string;\n public readonly provider: string;\n public readonly capabilities: ModelCapabilities;\n public readonly pricing?: ModelPricing;\n\n private readonly client: OpenAI;\n private readonly config: OpenAIModelConfig;\n private readonly logger: Logger = log;\n\n public constructor(client: OpenAI, config: OpenAIModelConfig, provider: string = \"openai\") {\n this.client = client;\n this.config = config;\n this.name = config.name;\n this.provider = provider;\n this.pricing = config.pricing;\n this.capabilities = {\n structuredOutput: config.structuredOutput ?? inferStructuredOutput(config.responseFormat),\n vision: config.vision ?? inferVisionCapability(config.name),\n // o-series + gpt-5 models surface a reasoning channel and accept\n // the `reasoning_effort` param. Explicit config wins over the\n // name-prefix inference.\n reasoning: config.reasoning ?? inferReasoningCapability(config.name),\n // OpenAI prompt caching is automatic on the Chat Completions API\n // (no caller-supplied breakpoints — the platform caches long\n // prompt prefixes server-side and reports the hit count via\n // `prompt_tokens_details.cached_tokens`). We therefore advertise\n // the read-side accounting capability as always available while\n // treating `ModelCallOptions.cacheControl` write breakpoints as a\n // no-op (see `buildReasoningParams` siblings — there is no cache\n // param to emit).\n promptCaching: true,\n };\n }\n\n /**\n * Single-shot completion. Sends the full message list to the Chat\n * Completions endpoint, waits for the terminal response, and reshapes it\n * into a vendor-neutral `ModelResponse`. Per-call `options` override the\n * instance's `ModelConfig` defaults for this call only.\n */\n public async complete(messages: Message[], options?: ModelCallOptions): Promise<ModelResponse> {\n // Per-call request/response logs are hot-path in production agents\n // — keep them at `debug` so `info` stays reserved for lifecycle\n // events (agent starting/completed, etc.). Operators who need to\n // audit every LLM call can raise log-level at runtime.\n this.logger.debug(LOG_MODULE, \"request\", \"Starting call to chat.completions\", {\n model: this.name,\n messageCount: messages.length,\n streaming: false,\n toolCount: options?.tools?.length ?? 0,\n });\n\n let response: OpenAI.Chat.Completions.ChatCompletion;\n\n try {\n response = await this.client.chat.completions.create(\n {\n model: this.name,\n messages: toOpenAIMessages(messages),\n temperature: options?.temperature ?? this.config.temperature,\n max_tokens: options?.maxTokens ?? this.config.maxTokens,\n tools: toOpenAITools(options?.tools),\n ...this.buildResponseFormat(options?.responseSchema),\n ...this.buildReasoningParams(options?.reasoning),\n },\n options?.signal ? { signal: options.signal } : undefined,\n );\n } catch (thrown) {\n const wrapped = wrapOpenAIError(thrown);\n\n this.logger.error(LOG_MODULE, \"error\", wrapped.message, {\n code: wrapped.code,\n context: wrapped.context,\n });\n\n throw wrapped;\n }\n\n const choice = response.choices[0];\n const finishReason = mapFinishReason(choice.finish_reason);\n const usage = this.extractUsage(response.usage);\n\n this.logger.debug(LOG_MODULE, \"response\", \"call to chat.completions succeeded\", {\n finishReason,\n usage,\n });\n\n return {\n content: choice.message.content ?? \"\",\n finishReason,\n usage,\n toolCalls: this.extractToolCalls(choice.message.tool_calls),\n };\n }\n\n /**\n * Incremental streaming completion. Yields neutral `ModelStreamChunk`s —\n * `delta` for text tokens, `tool-call` when the model requests a tool,\n * and a terminal `done` carrying the final finish reason + usage totals.\n * Callers consume it with `for await`.\n */\n public async *stream(\n messages: Message[],\n options?: ModelCallOptions,\n ): AsyncIterable<ModelStreamChunk> {\n this.logger.debug(LOG_MODULE, \"request\", \"Starting streaming call to chat.completions\", {\n model: this.name,\n messageCount: messages.length,\n streaming: true,\n toolCount: options?.tools?.length ?? 0,\n });\n\n let stream: Awaited<ReturnType<typeof this.client.chat.completions.create>>;\n\n try {\n stream = await this.client.chat.completions.create(\n {\n model: this.name,\n messages: toOpenAIMessages(messages),\n temperature: options?.temperature ?? this.config.temperature,\n max_tokens: options?.maxTokens ?? this.config.maxTokens,\n tools: toOpenAITools(options?.tools),\n stream: true,\n stream_options: { include_usage: true },\n ...this.buildResponseFormat(options?.responseSchema),\n ...this.buildReasoningParams(options?.reasoning),\n },\n options?.signal ? { signal: options.signal } : undefined,\n );\n } catch (thrown) {\n const wrapped = wrapOpenAIError(thrown);\n\n this.logger.error(LOG_MODULE, \"error\", wrapped.message, {\n code: wrapped.code,\n context: wrapped.context,\n });\n\n throw wrapped;\n }\n\n let rawFinishReason: string = \"stop\";\n const usage: Usage = { input: 0, output: 0, total: 0 };\n const toolCallAccum = new Map<number, { id: string; name: string; arguments: string }>();\n\n try {\n for await (const chunk of stream as AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>) {\n const delta = chunk.choices[0]?.delta;\n const finish = chunk.choices[0]?.finish_reason;\n\n if (delta?.content) {\n yield { type: \"delta\", content: delta.content };\n }\n\n if (delta?.tool_calls) {\n for (const toolCall of delta.tool_calls) {\n const idx = toolCall.index ?? 0;\n if (!toolCallAccum.has(idx)) {\n toolCallAccum.set(idx, { id: \"\", name: \"\", arguments: \"\" });\n }\n const acc = toolCallAccum.get(idx)!;\n if (toolCall.id) acc.id = toolCall.id;\n if (toolCall.function?.name) acc.name = toolCall.function.name;\n if (toolCall.function?.arguments) acc.arguments += toolCall.function.arguments;\n }\n }\n\n if (finish) {\n rawFinishReason = finish;\n }\n\n if (chunk.usage) {\n usage.input = chunk.usage.prompt_tokens ?? 0;\n usage.output = chunk.usage.completion_tokens ?? 0;\n usage.total = chunk.usage.total_tokens ?? 0;\n const cached = chunk.usage.prompt_tokens_details?.cached_tokens;\n if (cached !== undefined && cached > 0) {\n usage.cachedTokens = cached;\n }\n const reasoning = chunk.usage.completion_tokens_details?.reasoning_tokens;\n if (reasoning !== undefined && reasoning > 0) {\n usage.reasoningTokens = reasoning;\n }\n }\n }\n\n for (const acc of toolCallAccum.values()) {\n // Skip accumulators that never received a function name — those\n // are partial fragments the model started but never identified\n // (e.g. arguments-only deltas with no originating `id`/`name`).\n // Yielding them produces nameless tool-calls the agent runtime\n // can't dispatch and would mis-attribute as a registered tool.\n if (!acc.name) continue;\n\n yield {\n type: \"tool-call\",\n id: acc.id,\n name: acc.name,\n input: safeJsonParse<Record<string, unknown>>(acc.arguments, {}),\n };\n }\n } catch (thrown) {\n const wrapped = wrapOpenAIError(thrown);\n\n this.logger.error(LOG_MODULE, \"error\", wrapped.message, {\n code: wrapped.code,\n context: wrapped.context,\n });\n\n throw wrapped;\n }\n\n const finishReason = mapFinishReason(rawFinishReason);\n\n this.logger.debug(LOG_MODULE, \"response\", \"Streaming call to chat.completions succeeded\", {\n finishReason,\n usage,\n });\n\n yield { type: \"done\", finishReason, usage };\n }\n\n /**\n * Translate the neutral `responseSchema` option into OpenAI's\n * `response_format` parameter.\n *\n * When `config.responseFormat` is set, it wins: `\"text\"` emits no\n * `response_format` at all, `\"json_object\"` always picks the loose\n * mode, and `\"json_schema\"` picks strict mode (with the same\n * `isStrictCompatible` safety check — a malformed schema still\n * degrades to `json_object` rather than 400). The override exists\n * because some targets (older OpenAI models, OpenRouter routes,\n * Ollama OpenAI-compat) reject strict `json_schema` outright.\n *\n * When the override is omitted, uses strict `json_schema` mode\n * (token-level enforcement) only when the schema is a proper\n * root-object JSON Schema (`{ type: \"object\", properties: ... }`).\n * For anything else — malformed extractor output, non-object\n * schemas, or future shapes we haven't tested — falls back to loose\n * `json_object` mode, which guarantees *some* valid JSON without\n * enforcing shape. The agent's soft instruction already embeds the\n * schema text in the system prompt when the model declares no\n * native structured-output capability, so shape validation still\n * runs client-side via the Standard Schema `validate()` call.\n *\n * Returns an empty spread when no schema was supplied, so the caller\n * can unconditionally `...buildResponseFormat(...)` into the request.\n */\n private buildResponseFormat(responseSchema: Record<string, unknown> | undefined): {\n response_format?: OpenAI.Chat.Completions.ChatCompletionCreateParams[\"response_format\"];\n } {\n if (!responseSchema) {\n return {};\n }\n\n const override = this.config.responseFormat;\n\n if (override === \"text\") {\n return {};\n }\n\n if (override === \"json_object\") {\n return { response_format: { type: \"json_object\" } };\n }\n\n // Either auto-select (no override) or explicit `\"json_schema\"`.\n // The strict-compat check still applies in the explicit case —\n // a malformed / non-object schema would 400 before sampling, so\n // we degrade to `json_object` rather than crash.\n if (this.isStrictCompatible(responseSchema)) {\n return {\n response_format: {\n type: \"json_schema\",\n json_schema: {\n name: \"response\",\n schema: responseSchema,\n strict: true,\n },\n },\n };\n }\n\n return { response_format: { type: \"json_object\" } };\n }\n\n /**\n * OpenAI strict `json_schema` mode requires the root to be a JSON\n * Schema object type (`{ type: \"object\", properties: ... }`). Anything\n * else (top-level arrays, primitives, unknown shapes) is rejected with\n * a 400 before a token is sampled. We check structurally here so the\n * first call doesn't crash on a malformed extraction — loose\n * `json_object` mode is a safe degradation.\n */\n private isStrictCompatible(schema: Record<string, unknown>): boolean {\n return (\n schema.type === \"object\" &&\n typeof schema.properties === \"object\" &&\n schema.properties !== null &&\n this.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\n * as nullable, e.g. `type: [\"string\", \"null\"]`, and still appear in\n * `required`). A schema that violates this anywhere in the tree is NOT\n * sent in strict `json_schema` mode — it degrades to loose\n * `json_object` so a hand-built or optional-bearing schema can't 400\n * the call (\"'required' ... must include every key in properties\").\n * Client-side `validate()` still enforces the full shape.\n */\n private 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 (!this.isStrictSafeNode(properties[key])) {\n return false;\n }\n }\n }\n\n if (record.items !== undefined && !this.isStrictSafeNode(record.items)) {\n return false;\n }\n\n for (const branch of [\"anyOf\", \"allOf\", \"oneOf\"] as const) {\n const value = record[branch];\n if (Array.isArray(value) && value.some((sub) => !this.isStrictSafeNode(sub))) {\n return false;\n }\n }\n\n return true;\n }\n\n /**\n * Normalize OpenAI's `usage` block (which may be absent on some responses\n * or partials) into the neutral `Usage` shape. Missing usage collapses to\n * zeros rather than propagating `undefined`, so downstream aggregation\n * math stays safe.\n *\n * `cachedTokens` mirrors `prompt_tokens_details.cached_tokens` (the\n * subset of the prompt served from OpenAI's automatic prompt cache);\n * `reasoningTokens` mirrors `completion_tokens_details.reasoning_tokens`\n * (the hidden reasoning channel on o-series / gpt-5 models, already\n * counted within `output`). Both are emitted only when the provider\n * reports a positive value, so non-reasoning / uncached calls keep the\n * lean `{ input, output, total }` shape.\n */\n private extractUsage(raw: OpenAI.Completions.CompletionUsage | undefined): Usage {\n if (!raw) {\n return { input: 0, output: 0, total: 0 };\n }\n\n const cachedTokens = raw.prompt_tokens_details?.cached_tokens;\n const reasoningTokens = raw.completion_tokens_details?.reasoning_tokens;\n\n return {\n input: raw.prompt_tokens,\n output: raw.completion_tokens,\n total: raw.total_tokens,\n ...(cachedTokens !== undefined && cachedTokens > 0 ? { cachedTokens } : {}),\n ...(reasoningTokens !== undefined && reasoningTokens > 0 ? { reasoningTokens } : {}),\n };\n }\n\n /**\n * Translate the neutral `ModelCallOptions.reasoning` hint into OpenAI's\n * `reasoning_effort` request param. Only `effort` maps — OpenAI's Chat\n * Completions API exposes a discrete effort knob, not a token budget,\n * so `reasoning.maxTokens` (the Anthropic extended-thinking cap) has no\n * wire equivalent here and is silently ignored.\n *\n * No-ops in two cases so the adapter never forwards an unsupported\n * param: (1) the model is not reasoning-capable\n * (`capabilities.reasoning` is false — e.g. `gpt-4o`), or (2) the caller\n * supplied no `effort`. The neutral `ReasoningEffort`\n * (`\"low\" | \"medium\" | \"high\"`) is a strict subset of OpenAI's accepted\n * values, so it forwards verbatim.\n *\n * Returns an empty spread when nothing applies, so the caller can\n * unconditionally `...buildReasoningParams(...)` into the request.\n */\n private buildReasoningParams(reasoning: ModelCallOptions[\"reasoning\"]): {\n reasoning_effort?: OpenAI.Chat.Completions.ChatCompletionCreateParams[\"reasoning_effort\"];\n } {\n if (!this.capabilities.reasoning || !reasoning?.effort) {\n return {};\n }\n\n return { reasoning_effort: reasoning.effort };\n }\n\n /**\n * Reshape OpenAI's `tool_calls` array into the neutral\n * `ModelToolCallRequest[]`. The raw `arguments` field is a JSON string\n * per OpenAI's protocol — we parse it defensively via `safeJsonParse` so\n * malformed or empty arguments yield an empty object instead of crashing\n * the trip. Returns `undefined` when no tools were requested so callers\n * can branch on presence.\n */\n private extractToolCalls(\n rawToolCalls: OpenAI.Chat.Completions.ChatCompletionMessageToolCall[] | undefined,\n ): ModelToolCallRequest[] | undefined {\n if (!rawToolCalls || rawToolCalls.length === 0) {\n return undefined;\n }\n\n return rawToolCalls.map((toolCall) => ({\n id: toolCall.id,\n name: (toolCall as any).function.name,\n input: safeJsonParse<Record<string, unknown>>((toolCall as any).function.arguments, {}),\n }));\n }\n}\n","import OpenAI from \"openai\";\nimport type {\n EmbedderContract,\n ModelContract,\n ModelPricing,\n SDKAdapterContract,\n} from \"@warlock.js/ai\";\nimport { approximateTokenCount } from \"@warlock.js/ai\";\nimport type { OpenAIEmbedderConfig, OpenAIModelConfig, OpenAISDKConfig } from \"./config.type\";\nimport { OpenAIEmbedder } from \"./embedder\";\nimport { OpenAIModel } from \"./model\";\n\n/**\n * OpenAI-backed implementation of `SDKAdapterContract`.\n *\n * **Role.** The package entry point for any OpenAI-compatible provider\n * (OpenAI, Azure OpenAI, OpenRouter, local gateways speaking the Chat\n * Completions protocol). A single `OpenAISDK` instance holds one live\n * `OpenAI` client, shared by every `ModelContract` it produces via\n * `model()`. Users construct one SDK per provider/account and reuse it\n * across all agents, workflows, and supervisors that target that\n * provider.\n *\n * **Responsibility.**\n * - Owns: a long-lived `OpenAI` client (authentication, base URL) and\n * its lifetime scope. Factory for `OpenAIModel` instances — each\n * model call gets a reference to the same client.\n * - Does NOT own: anything per-call (tool execution, message history,\n * streaming loop) — those live in `OpenAIModel` and the agent runtime.\n *\n * Modeled as a class (see §4.2 of code-style.md — \"long-lived state\n * across many calls\"): the `OpenAI` client is heavy to construct and\n * designed to be reused; keeping it on `this` makes that reuse\n * explicit and aligns with the PascalCase naming convention readers\n * expect from a constructor.\n *\n * @example\n * const openai = new OpenAISDK({ apiKey: process.env.OPENAI_API_KEY! });\n * const model = openai.model({ name: \"gpt-4o\", temperature: 0.7 });\n * const tokens = await openai.count(\"Hello world\");\n *\n * @example\n * // Compose into an `ai.openai` namespace for ergonomic agent wiring\n * const ai = { agent, tool, systemPrompt, persona, instruction, openai: new OpenAISDK({ apiKey }) };\n * const myAgent = ai.agent({ model: ai.openai.model({ name: \"gpt-4o-mini\" }) });\n */\nexport class OpenAISDK implements SDKAdapterContract {\n private readonly client: OpenAI;\n private readonly provider: string;\n private readonly pricing?: Record<string, ModelPricing>;\n\n public constructor(config: OpenAISDKConfig) {\n this.client = new OpenAI({\n apiKey: config.apiKey,\n baseURL: config.baseURL,\n });\n this.provider = config.provider ?? \"openai\";\n this.pricing = config.pricing;\n }\n\n /**\n * Build an `OpenAIModel` bound to this SDK's client. Each call returns\n * a fresh model instance, but all instances share the underlying\n * `OpenAI` client — connection pools, rate limits, and authentication\n * state stay unified across every model produced here. The SDK's\n * `provider` label is forwarded so every model self-identifies as\n * coming from the same upstream.\n *\n * Pricing resolution: per-model `config.pricing` wins; otherwise the\n * SDK-level registry entry keyed by `config.name`; otherwise\n * `undefined` (no cost computed).\n */\n public model(config: OpenAIModelConfig): ModelContract {\n const resolvedPricing = config.pricing ?? this.pricing?.[config.name];\n const resolvedConfig: OpenAIModelConfig =\n resolvedPricing === config.pricing ? config : { ...config, pricing: resolvedPricing };\n\n return new OpenAIModel(this.client, resolvedConfig, this.provider);\n }\n\n /**\n * Rough token-count estimate for a given text. Uses a\n * character-heuristic (`approximateTokenCount`) from the core package\n * — good enough for budgeting and quota guards, not for billing.\n * Accepts an optional model id for future per-model tokenizer\n * dispatch; currently ignored.\n */\n public async count(text: string, _model?: string): Promise<number> {\n return approximateTokenCount(text);\n }\n\n /**\n * Build an `OpenAIEmbedder` bound to this SDK's client. Each call\n * returns a fresh embedder instance sharing the same underlying\n * `OpenAI` client — connection pools and authentication stay unified\n * across every embedder produced here.\n *\n * @example\n * const embedder = openai.embedder({ name: \"text-embedding-3-small\" });\n * const { vector } = await embedder.embed(\"Hello world\");\n */\n public embedder(config: OpenAIEmbedderConfig): EmbedderContract {\n return new OpenAIEmbedder(this.client, config);\n }\n}\n"],"mappings":"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"}
@@ -96,6 +96,17 @@ type OpenAIModelConfig = ModelConfig & {
96
96
  * `responseFormat`.
97
97
  */
98
98
  structuredOutput?: boolean;
99
+ /**
100
+ * Override the auto-inferred `reasoning` capability. When omitted,
101
+ * the adapter checks the model name against a known-prefix list (see
102
+ * `known-reasoning-models.ts`) — `true` for the o-series (`o1*`,
103
+ * `o3*`, `o4*`) and the `gpt-5*` family, `false` otherwise. Setting
104
+ * this explicitly always wins over inference — useful for fine-tuned
105
+ * reasoning models or gateways exposing reasoning under a custom
106
+ * name. When `false`, `ModelCallOptions.reasoning` is ignored rather
107
+ * than forwarded as an unsupported `reasoning_effort` param.
108
+ */
109
+ reasoning?: boolean;
99
110
  };
100
111
  /**
101
112
  * Per-embedder configuration for `OpenAISDK.embedder()`. Mirrors the
@@ -1 +1 @@
1
- {"version":3,"file":"config.type.d.mts","names":[],"sources":["../../../../../../@warlock.js/ai-openai/src/config.type.ts"],"mappings":";;;;;;AAiCA;;;;;;;;;;;;;;AAOuC;AAiBvC;;;;AAAgC;AAUhC;;;;;;;;KAlCY,eAAA,GAAkB,aAAA;EAC5B,QAAA;EAgEgB;AAalB;;;;EAvEE,OAAA,GAAU,MAAA,SAAe,YAAA;AAAA;;;;;;;;;;;;;;;KAiBf,oBAAA;;;;;;;;;KAUA,iBAAA,GAAoB,WAAA;;;;;;;;EAQ9B,MAAA;;;;;;;;;;;;;;EAcA,cAAA,GAAiB,oBAAoB;;;;;;;;;EASrC,gBAAA;AAAA;;;;;;;;;;;KAaU,oBAAA,GAAuB,cAAc"}
1
+ {"version":3,"file":"config.type.d.mts","names":[],"sources":["../../../../../../@warlock.js/ai-openai/src/config.type.ts"],"mappings":";;;;;;AAiCA;;;;;;;;;;;;;;AAOuC;AAiBvC;;;;AAAgC;AAUhC;;;;;;;;KAlCY,eAAA,GAAkB,aAAA;EAC5B,QAAA;EA2ES;AAAA;AAaX;;;EAlFE,OAAA,GAAU,MAAA,SAAe,YAAA;AAAA;;;;;;;;;;;;;;;KAiBf,oBAAA;;;;;;;;;KAUA,iBAAA,GAAoB,WAAA;;;;;;;;EAQ9B,MAAA;;;;;;;;;;;;;;EAcA,cAAA,GAAiB,oBAAoB;;;;;;;;;EASrC,gBAAA;;;;;;;;;;;EAWA,SAAA;AAAA;;;;;;;;;;;KAaU,oBAAA,GAAuB,cAAc"}
@@ -0,0 +1,42 @@
1
+ //#region ../@warlock.js/ai-openai/src/known-reasoning-models.ts
2
+ /**
3
+ * Model-name prefixes for OpenAI families that expose internal
4
+ * reasoning / thinking tokens and accept the `reasoning_effort`
5
+ * request parameter on the Chat Completions API.
6
+ *
7
+ * Matched as a prefix so dated variants (`o3-2025-04-16`) and
8
+ * `-mini` / `-pro` suffixes (`o4-mini`, `gpt-5-pro`) are covered
9
+ * without listing every release tag explicitly.
10
+ *
11
+ * Maintenance: append a new prefix when OpenAI ships a reasoning
12
+ * model family that doesn't already match. Devs can always override
13
+ * per-model via `openai.model({ name, reasoning: true | false })` —
14
+ * explicit config wins over inference in either direction.
15
+ */
16
+ const REASONING_CAPABLE_PREFIXES = [
17
+ "o1",
18
+ "o3",
19
+ "o4",
20
+ "gpt-5"
21
+ ];
22
+ /**
23
+ * Infer whether a given OpenAI model name is a reasoning model (o-series
24
+ * and the gpt-5 family) based on the known-prefix list. Unknown models
25
+ * default to `false` so the adapter never forwards an unsupported
26
+ * `reasoning_effort` param to a non-reasoning model (which would 400).
27
+ *
28
+ * @example
29
+ * inferReasoningCapability("o3-mini"); // → true
30
+ * inferReasoningCapability("o4-mini"); // → true
31
+ * inferReasoningCapability("gpt-5-pro"); // → true
32
+ * inferReasoningCapability("gpt-4o"); // → false
33
+ * inferReasoningCapability("custom-llm"); // → false
34
+ */
35
+ function inferReasoningCapability(modelName) {
36
+ const normalized = modelName.toLowerCase();
37
+ return REASONING_CAPABLE_PREFIXES.some((prefix) => normalized.startsWith(prefix));
38
+ }
39
+
40
+ //#endregion
41
+ export { inferReasoningCapability };
42
+ //# sourceMappingURL=known-reasoning-models.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"known-reasoning-models.mjs","names":[],"sources":["../../../../../../@warlock.js/ai-openai/src/known-reasoning-models.ts"],"sourcesContent":["/**\n * Model-name prefixes for OpenAI families that expose internal\n * reasoning / thinking tokens and accept the `reasoning_effort`\n * request parameter on the Chat Completions API.\n *\n * Matched as a prefix so dated variants (`o3-2025-04-16`) and\n * `-mini` / `-pro` suffixes (`o4-mini`, `gpt-5-pro`) are covered\n * without listing every release tag explicitly.\n *\n * Maintenance: append a new prefix when OpenAI ships a reasoning\n * model family that doesn't already match. Devs can always override\n * per-model via `openai.model({ name, reasoning: true | false })` —\n * explicit config wins over inference in either direction.\n */\nconst REASONING_CAPABLE_PREFIXES = [\"o1\", \"o3\", \"o4\", \"gpt-5\"];\n\n/**\n * Infer whether a given OpenAI model name is a reasoning model (o-series\n * and the gpt-5 family) based on the known-prefix list. Unknown models\n * default to `false` so the adapter never forwards an unsupported\n * `reasoning_effort` param to a non-reasoning model (which would 400).\n *\n * @example\n * inferReasoningCapability(\"o3-mini\"); // → true\n * inferReasoningCapability(\"o4-mini\"); // → true\n * inferReasoningCapability(\"gpt-5-pro\"); // → true\n * inferReasoningCapability(\"gpt-4o\"); // → false\n * inferReasoningCapability(\"custom-llm\"); // → false\n */\nexport function inferReasoningCapability(modelName: string): boolean {\n const normalized = modelName.toLowerCase();\n\n return REASONING_CAPABLE_PREFIXES.some((prefix) => normalized.startsWith(prefix));\n}\n"],"mappings":";;;;;;;;;;;;;;;AAcA,MAAM,6BAA6B;CAAC;CAAM;CAAM;CAAM;AAAO;;;;;;;;;;;;;;AAe7D,SAAgB,yBAAyB,WAA4B;CACnE,MAAM,aAAa,UAAU,YAAY;CAEzC,OAAO,2BAA2B,MAAM,WAAW,WAAW,WAAW,MAAM,CAAC;AAClF"}
package/esm/model.mjs CHANGED
@@ -3,6 +3,7 @@ import { toOpenAIMessages } from "./utils/to-openai-messages.mjs";
3
3
  import { toOpenAITools } from "./utils/to-openai-tools.mjs";
4
4
  import { wrapOpenAIError } from "./utils/wrap-openai-error.mjs";
5
5
  import "./utils/index.mjs";
6
+ import { inferReasoningCapability } from "./known-reasoning-models.mjs";
6
7
  import { inferVisionCapability } from "./known-vision-models.mjs";
7
8
  import { safeJsonParse } from "@warlock.js/ai";
8
9
  import { log } from "@warlock.js/logger";
@@ -69,7 +70,9 @@ var OpenAIModel = class {
69
70
  this.pricing = config.pricing;
70
71
  this.capabilities = {
71
72
  structuredOutput: config.structuredOutput ?? inferStructuredOutput(config.responseFormat),
72
- vision: config.vision ?? inferVisionCapability(config.name)
73
+ vision: config.vision ?? inferVisionCapability(config.name),
74
+ reasoning: config.reasoning ?? inferReasoningCapability(config.name),
75
+ promptCaching: true
73
76
  };
74
77
  }
75
78
  /**
@@ -93,7 +96,8 @@ var OpenAIModel = class {
93
96
  temperature: options?.temperature ?? this.config.temperature,
94
97
  max_tokens: options?.maxTokens ?? this.config.maxTokens,
95
98
  tools: toOpenAITools(options?.tools),
96
- ...this.buildResponseFormat(options?.responseSchema)
99
+ ...this.buildResponseFormat(options?.responseSchema),
100
+ ...this.buildReasoningParams(options?.reasoning)
97
101
  }, options?.signal ? { signal: options.signal } : void 0);
98
102
  } catch (thrown) {
99
103
  const wrapped = wrapOpenAIError(thrown);
@@ -140,7 +144,8 @@ var OpenAIModel = class {
140
144
  tools: toOpenAITools(options?.tools),
141
145
  stream: true,
142
146
  stream_options: { include_usage: true },
143
- ...this.buildResponseFormat(options?.responseSchema)
147
+ ...this.buildResponseFormat(options?.responseSchema),
148
+ ...this.buildReasoningParams(options?.reasoning)
144
149
  }, options?.signal ? { signal: options.signal } : void 0);
145
150
  } catch (thrown) {
146
151
  const wrapped = wrapOpenAIError(thrown);
@@ -184,6 +189,8 @@ var OpenAIModel = class {
184
189
  usage.total = chunk.usage.total_tokens ?? 0;
185
190
  const cached = chunk.usage.prompt_tokens_details?.cached_tokens;
186
191
  if (cached !== void 0 && cached > 0) usage.cachedTokens = cached;
192
+ const reasoning = chunk.usage.completion_tokens_details?.reasoning_tokens;
193
+ if (reasoning !== void 0 && reasoning > 0) usage.reasoningTokens = reasoning;
187
194
  }
188
195
  }
189
196
  for (const acc of toolCallAccum.values()) {
@@ -264,13 +271,53 @@ var OpenAIModel = class {
264
271
  * `json_object` mode is a safe degradation.
265
272
  */
266
273
  isStrictCompatible(schema) {
267
- return schema.type === "object" && typeof schema.properties === "object" && schema.properties !== null;
274
+ return schema.type === "object" && typeof schema.properties === "object" && schema.properties !== null && this.isStrictSafeNode(schema);
275
+ }
276
+ /**
277
+ * Recursively check the one strict-mode rule schemas most often trip on:
278
+ * every object must list ALL of its `properties` in `required` (OpenAI
279
+ * strict has no notion of optional — optional fields must be expressed
280
+ * as nullable, e.g. `type: ["string", "null"]`, and still appear in
281
+ * `required`). A schema that violates this anywhere in the tree is NOT
282
+ * sent in strict `json_schema` mode — it degrades to loose
283
+ * `json_object` so a hand-built or optional-bearing schema can't 400
284
+ * the call ("'required' ... must include every key in properties").
285
+ * Client-side `validate()` still enforces the full shape.
286
+ */
287
+ isStrictSafeNode(node) {
288
+ if (!node || typeof node !== "object") return true;
289
+ const record = node;
290
+ if (record.type === "object" && record.properties && typeof record.properties === "object") {
291
+ const properties = record.properties;
292
+ const keys = Object.keys(properties);
293
+ const required = Array.isArray(record.required) ? record.required : [];
294
+ if (keys.some((key) => !required.includes(key))) return false;
295
+ for (const key of keys) if (!this.isStrictSafeNode(properties[key])) return false;
296
+ }
297
+ if (record.items !== void 0 && !this.isStrictSafeNode(record.items)) return false;
298
+ for (const branch of [
299
+ "anyOf",
300
+ "allOf",
301
+ "oneOf"
302
+ ]) {
303
+ const value = record[branch];
304
+ if (Array.isArray(value) && value.some((sub) => !this.isStrictSafeNode(sub))) return false;
305
+ }
306
+ return true;
268
307
  }
269
308
  /**
270
309
  * Normalize OpenAI's `usage` block (which may be absent on some responses
271
310
  * or partials) into the neutral `Usage` shape. Missing usage collapses to
272
311
  * zeros rather than propagating `undefined`, so downstream aggregation
273
312
  * math stays safe.
313
+ *
314
+ * `cachedTokens` mirrors `prompt_tokens_details.cached_tokens` (the
315
+ * subset of the prompt served from OpenAI's automatic prompt cache);
316
+ * `reasoningTokens` mirrors `completion_tokens_details.reasoning_tokens`
317
+ * (the hidden reasoning channel on o-series / gpt-5 models, already
318
+ * counted within `output`). Both are emitted only when the provider
319
+ * reports a positive value, so non-reasoning / uncached calls keep the
320
+ * lean `{ input, output, total }` shape.
274
321
  */
275
322
  extractUsage(raw) {
276
323
  if (!raw) return {
@@ -279,14 +326,37 @@ var OpenAIModel = class {
279
326
  total: 0
280
327
  };
281
328
  const cachedTokens = raw.prompt_tokens_details?.cached_tokens;
329
+ const reasoningTokens = raw.completion_tokens_details?.reasoning_tokens;
282
330
  return {
283
331
  input: raw.prompt_tokens,
284
332
  output: raw.completion_tokens,
285
333
  total: raw.total_tokens,
286
- ...cachedTokens !== void 0 && cachedTokens > 0 ? { cachedTokens } : {}
334
+ ...cachedTokens !== void 0 && cachedTokens > 0 ? { cachedTokens } : {},
335
+ ...reasoningTokens !== void 0 && reasoningTokens > 0 ? { reasoningTokens } : {}
287
336
  };
288
337
  }
289
338
  /**
339
+ * Translate the neutral `ModelCallOptions.reasoning` hint into OpenAI's
340
+ * `reasoning_effort` request param. Only `effort` maps — OpenAI's Chat
341
+ * Completions API exposes a discrete effort knob, not a token budget,
342
+ * so `reasoning.maxTokens` (the Anthropic extended-thinking cap) has no
343
+ * wire equivalent here and is silently ignored.
344
+ *
345
+ * No-ops in two cases so the adapter never forwards an unsupported
346
+ * param: (1) the model is not reasoning-capable
347
+ * (`capabilities.reasoning` is false — e.g. `gpt-4o`), or (2) the caller
348
+ * supplied no `effort`. The neutral `ReasoningEffort`
349
+ * (`"low" | "medium" | "high"`) is a strict subset of OpenAI's accepted
350
+ * values, so it forwards verbatim.
351
+ *
352
+ * Returns an empty spread when nothing applies, so the caller can
353
+ * unconditionally `...buildReasoningParams(...)` into the request.
354
+ */
355
+ buildReasoningParams(reasoning) {
356
+ if (!this.capabilities.reasoning || !reasoning?.effort) return {};
357
+ return { reasoning_effort: reasoning.effort };
358
+ }
359
+ /**
290
360
  * Reshape OpenAI's `tool_calls` array into the neutral
291
361
  * `ModelToolCallRequest[]`. The raw `arguments` field is a JSON string
292
362
  * per OpenAI's protocol — we parse it defensively via `safeJsonParse` so
package/esm/model.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"model.mjs","names":[],"sources":["../../../../../../@warlock.js/ai-openai/src/model.ts"],"sourcesContent":["import {\n safeJsonParse,\n type Message,\n type ModelCallOptions,\n type ModelCapabilities,\n type ModelContract,\n type ModelPricing,\n type ModelResponse,\n type ModelStreamChunk,\n type ModelToolCallRequest,\n type Usage,\n} from \"@warlock.js/ai\";\nimport { log, type Logger } from \"@warlock.js/logger\";\nimport type OpenAI from \"openai\";\nimport type { OpenAIModelConfig, OpenAIResponseFormat } from \"./config.type\";\nimport { inferVisionCapability } from \"./known-vision-models\";\nimport { mapFinishReason, toOpenAIMessages, toOpenAITools, wrapOpenAIError } from \"./utils\";\n\nconst LOG_MODULE = \"ai.openai\";\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 */\nfunction inferStructuredOutput(responseFormat: OpenAIResponseFormat | undefined): boolean {\n if (responseFormat === \"json_object\" || responseFormat === \"text\") {\n return false;\n }\n\n return true;\n}\n\n/**\n * OpenAI-backed implementation of `ModelContract`.\n *\n * **Role.** The provider-facing bridge between the vendor-neutral\n * `@warlock.js/ai` agent runtime and the official `openai` SDK. Agents,\n * workflows, and supervisors never talk to OpenAI directly — they hold a\n * `ModelContract`, and this class is what makes that contract concrete for\n * any OpenAI-compatible endpoint (OpenAI, Azure OpenAI, OpenRouter, local\n * gateways that speak the Chat Completions protocol).\n *\n * **Responsibility.**\n * - Owns: a long-lived `OpenAI` client + frozen `ModelConfig` (name,\n * temperature, maxTokens) used as defaults for every call.\n * - Owns: translating vendor-neutral `Message[]` and\n * `ToolContract[]` into OpenAI wire shapes on the way out, and\n * translating OpenAI's response (content, finish reason, tool calls,\n * usage) back into the neutral shapes on the way in.\n * - Does NOT own: dispatching tools, deciding whether to loop, tracking\n * conversation history, or retrying on failure — those are agent\n * concerns. The model is a stateless (per-call) protocol adapter.\n *\n * Because it holds a live client and shared defaults, it is modeled as a\n * class (see §4.2 of code-style.md — \"long-lived state across calls\").\n *\n * @example\n * import OpenAI from \"openai\";\n * const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });\n * const model = new OpenAIModel(client, { name: \"gpt-4o\", temperature: 0.3 });\n *\n * const myAgent = agent({\n * model,\n * systemPrompt: \"You are a helpful assistant.\",\n * tools: [searchTool],\n * });\n *\n * const result = await myAgent.execute(\"Summarize today's news.\");\n */\nexport class OpenAIModel implements ModelContract {\n public readonly name: string;\n public readonly provider: string;\n public readonly capabilities: ModelCapabilities;\n public readonly pricing?: ModelPricing;\n\n private readonly client: OpenAI;\n private readonly config: OpenAIModelConfig;\n private readonly logger: Logger = log;\n\n public constructor(client: OpenAI, config: OpenAIModelConfig, provider: string = \"openai\") {\n this.client = client;\n this.config = config;\n this.name = config.name;\n this.provider = provider;\n this.pricing = config.pricing;\n this.capabilities = {\n structuredOutput: config.structuredOutput ?? inferStructuredOutput(config.responseFormat),\n vision: config.vision ?? inferVisionCapability(config.name),\n };\n }\n\n /**\n * Single-shot completion. Sends the full message list to the Chat\n * Completions endpoint, waits for the terminal response, and reshapes it\n * into a vendor-neutral `ModelResponse`. Per-call `options` override the\n * instance's `ModelConfig` defaults for this call only.\n */\n public async complete(messages: Message[], options?: ModelCallOptions): Promise<ModelResponse> {\n // Per-call request/response logs are hot-path in production agents\n // — keep them at `debug` so `info` stays reserved for lifecycle\n // events (agent starting/completed, etc.). Operators who need to\n // audit every LLM call can raise log-level at runtime.\n this.logger.debug(LOG_MODULE, \"request\", \"Starting call to chat.completions\", {\n model: this.name,\n messageCount: messages.length,\n streaming: false,\n toolCount: options?.tools?.length ?? 0,\n });\n\n let response: OpenAI.Chat.Completions.ChatCompletion;\n\n try {\n response = await this.client.chat.completions.create(\n {\n model: this.name,\n messages: toOpenAIMessages(messages),\n temperature: options?.temperature ?? this.config.temperature,\n max_tokens: options?.maxTokens ?? this.config.maxTokens,\n tools: toOpenAITools(options?.tools),\n ...this.buildResponseFormat(options?.responseSchema),\n },\n options?.signal ? { signal: options.signal } : undefined,\n );\n } catch (thrown) {\n const wrapped = wrapOpenAIError(thrown);\n\n this.logger.error(LOG_MODULE, \"error\", wrapped.message, {\n code: wrapped.code,\n context: wrapped.context,\n });\n\n throw wrapped;\n }\n\n const choice = response.choices[0];\n const finishReason = mapFinishReason(choice.finish_reason);\n const usage = this.extractUsage(response.usage);\n\n this.logger.debug(LOG_MODULE, \"response\", \"call to chat.completions succeeded\", {\n finishReason,\n usage,\n });\n\n return {\n content: choice.message.content ?? \"\",\n finishReason,\n usage,\n toolCalls: this.extractToolCalls(choice.message.tool_calls),\n };\n }\n\n /**\n * Incremental streaming completion. Yields neutral `ModelStreamChunk`s —\n * `delta` for text tokens, `tool-call` when the model requests a tool,\n * and a terminal `done` carrying the final finish reason + usage totals.\n * Callers consume it with `for await`.\n */\n public async *stream(\n messages: Message[],\n options?: ModelCallOptions,\n ): AsyncIterable<ModelStreamChunk> {\n this.logger.debug(LOG_MODULE, \"request\", \"Starting streaming call to chat.completions\", {\n model: this.name,\n messageCount: messages.length,\n streaming: true,\n toolCount: options?.tools?.length ?? 0,\n });\n\n let stream: Awaited<ReturnType<typeof this.client.chat.completions.create>>;\n\n try {\n stream = await this.client.chat.completions.create(\n {\n model: this.name,\n messages: toOpenAIMessages(messages),\n temperature: options?.temperature ?? this.config.temperature,\n max_tokens: options?.maxTokens ?? this.config.maxTokens,\n tools: toOpenAITools(options?.tools),\n stream: true,\n stream_options: { include_usage: true },\n ...this.buildResponseFormat(options?.responseSchema),\n },\n options?.signal ? { signal: options.signal } : undefined,\n );\n } catch (thrown) {\n const wrapped = wrapOpenAIError(thrown);\n\n this.logger.error(LOG_MODULE, \"error\", wrapped.message, {\n code: wrapped.code,\n context: wrapped.context,\n });\n\n throw wrapped;\n }\n\n let rawFinishReason: string = \"stop\";\n const usage: Usage = { input: 0, output: 0, total: 0 };\n const toolCallAccum = new Map<number, { id: string; name: string; arguments: string }>();\n\n try {\n for await (const chunk of stream as AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>) {\n const delta = chunk.choices[0]?.delta;\n const finish = chunk.choices[0]?.finish_reason;\n\n if (delta?.content) {\n yield { type: \"delta\", content: delta.content };\n }\n\n if (delta?.tool_calls) {\n for (const toolCall of delta.tool_calls) {\n const idx = toolCall.index ?? 0;\n if (!toolCallAccum.has(idx)) {\n toolCallAccum.set(idx, { id: \"\", name: \"\", arguments: \"\" });\n }\n const acc = toolCallAccum.get(idx)!;\n if (toolCall.id) acc.id = toolCall.id;\n if (toolCall.function?.name) acc.name = toolCall.function.name;\n if (toolCall.function?.arguments) acc.arguments += toolCall.function.arguments;\n }\n }\n\n if (finish) {\n rawFinishReason = finish;\n }\n\n if (chunk.usage) {\n usage.input = chunk.usage.prompt_tokens ?? 0;\n usage.output = chunk.usage.completion_tokens ?? 0;\n usage.total = chunk.usage.total_tokens ?? 0;\n const cached = chunk.usage.prompt_tokens_details?.cached_tokens;\n if (cached !== undefined && cached > 0) {\n usage.cachedTokens = cached;\n }\n }\n }\n\n for (const acc of toolCallAccum.values()) {\n // Skip accumulators that never received a function name — those\n // are partial fragments the model started but never identified\n // (e.g. arguments-only deltas with no originating `id`/`name`).\n // Yielding them produces nameless tool-calls the agent runtime\n // can't dispatch and would mis-attribute as a registered tool.\n if (!acc.name) continue;\n\n yield {\n type: \"tool-call\",\n id: acc.id,\n name: acc.name,\n input: safeJsonParse<Record<string, unknown>>(acc.arguments, {}),\n };\n }\n } catch (thrown) {\n const wrapped = wrapOpenAIError(thrown);\n\n this.logger.error(LOG_MODULE, \"error\", wrapped.message, {\n code: wrapped.code,\n context: wrapped.context,\n });\n\n throw wrapped;\n }\n\n const finishReason = mapFinishReason(rawFinishReason);\n\n this.logger.debug(LOG_MODULE, \"response\", \"Streaming call to chat.completions succeeded\", {\n finishReason,\n usage,\n });\n\n yield { type: \"done\", finishReason, usage };\n }\n\n /**\n * Translate the neutral `responseSchema` option into OpenAI's\n * `response_format` parameter.\n *\n * When `config.responseFormat` is set, it wins: `\"text\"` emits no\n * `response_format` at all, `\"json_object\"` always picks the loose\n * mode, and `\"json_schema\"` picks strict mode (with the same\n * `isStrictCompatible` safety check — a malformed schema still\n * degrades to `json_object` rather than 400). The override exists\n * because some targets (older OpenAI models, OpenRouter routes,\n * Ollama OpenAI-compat) reject strict `json_schema` outright.\n *\n * When the override is omitted, uses strict `json_schema` mode\n * (token-level enforcement) only when the schema is a proper\n * root-object JSON Schema (`{ type: \"object\", properties: ... }`).\n * For anything else — malformed extractor output, non-object\n * schemas, or future shapes we haven't tested — falls back to loose\n * `json_object` mode, which guarantees *some* valid JSON without\n * enforcing shape. The agent's soft instruction already embeds the\n * schema text in the system prompt when the model declares no\n * native structured-output capability, so shape validation still\n * runs client-side via the Standard Schema `validate()` call.\n *\n * Returns an empty spread when no schema was supplied, so the caller\n * can unconditionally `...buildResponseFormat(...)` into the request.\n */\n private buildResponseFormat(responseSchema: Record<string, unknown> | undefined): {\n response_format?: OpenAI.Chat.Completions.ChatCompletionCreateParams[\"response_format\"];\n } {\n if (!responseSchema) {\n return {};\n }\n\n const override = this.config.responseFormat;\n\n if (override === \"text\") {\n return {};\n }\n\n if (override === \"json_object\") {\n return { response_format: { type: \"json_object\" } };\n }\n\n // Either auto-select (no override) or explicit `\"json_schema\"`.\n // The strict-compat check still applies in the explicit case —\n // a malformed / non-object schema would 400 before sampling, so\n // we degrade to `json_object` rather than crash.\n if (this.isStrictCompatible(responseSchema)) {\n return {\n response_format: {\n type: \"json_schema\",\n json_schema: {\n name: \"response\",\n schema: responseSchema,\n strict: true,\n },\n },\n };\n }\n\n return { response_format: { type: \"json_object\" } };\n }\n\n /**\n * OpenAI strict `json_schema` mode requires the root to be a JSON\n * Schema object type (`{ type: \"object\", properties: ... }`). Anything\n * else (top-level arrays, primitives, unknown shapes) is rejected with\n * a 400 before a token is sampled. We check structurally here so the\n * first call doesn't crash on a malformed extraction — loose\n * `json_object` mode is a safe degradation.\n */\n private isStrictCompatible(schema: Record<string, unknown>): boolean {\n return (\n schema.type === \"object\" &&\n typeof schema.properties === \"object\" &&\n schema.properties !== null\n );\n }\n\n /**\n * Normalize OpenAI's `usage` block (which may be absent on some responses\n * or partials) into the neutral `Usage` shape. Missing usage collapses to\n * zeros rather than propagating `undefined`, so downstream aggregation\n * math stays safe.\n */\n private extractUsage(raw: OpenAI.Completions.CompletionUsage | undefined): Usage {\n if (!raw) {\n return { input: 0, output: 0, total: 0 };\n }\n\n const cachedTokens = raw.prompt_tokens_details?.cached_tokens;\n\n return {\n input: raw.prompt_tokens,\n output: raw.completion_tokens,\n total: raw.total_tokens,\n ...(cachedTokens !== undefined && cachedTokens > 0 ? { cachedTokens } : {}),\n };\n }\n\n /**\n * Reshape OpenAI's `tool_calls` array into the neutral\n * `ModelToolCallRequest[]`. The raw `arguments` field is a JSON string\n * per OpenAI's protocol — we parse it defensively via `safeJsonParse` so\n * malformed or empty arguments yield an empty object instead of crashing\n * the trip. Returns `undefined` when no tools were requested so callers\n * can branch on presence.\n */\n private extractToolCalls(\n rawToolCalls: OpenAI.Chat.Completions.ChatCompletionMessageToolCall[] | undefined,\n ): ModelToolCallRequest[] | undefined {\n if (!rawToolCalls || rawToolCalls.length === 0) {\n return undefined;\n }\n\n return rawToolCalls.map((toolCall) => ({\n id: toolCall.id,\n name: (toolCall as any).function.name,\n input: safeJsonParse<Record<string, unknown>>((toolCall as any).function.arguments, {}),\n }));\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"model.mjs","names":[],"sources":["../../../../../../@warlock.js/ai-openai/src/model.ts"],"sourcesContent":["import {\n safeJsonParse,\n type Message,\n type ModelCallOptions,\n type ModelCapabilities,\n type ModelContract,\n type ModelPricing,\n type ModelResponse,\n type ModelStreamChunk,\n type ModelToolCallRequest,\n type Usage,\n} from \"@warlock.js/ai\";\nimport { log, type Logger } from \"@warlock.js/logger\";\nimport type OpenAI from \"openai\";\nimport type { OpenAIModelConfig, OpenAIResponseFormat } from \"./config.type\";\nimport { inferReasoningCapability } from \"./known-reasoning-models\";\nimport { inferVisionCapability } from \"./known-vision-models\";\nimport { mapFinishReason, toOpenAIMessages, toOpenAITools, wrapOpenAIError } from \"./utils\";\n\nconst LOG_MODULE = \"ai.openai\";\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 */\nfunction inferStructuredOutput(responseFormat: OpenAIResponseFormat | undefined): boolean {\n if (responseFormat === \"json_object\" || responseFormat === \"text\") {\n return false;\n }\n\n return true;\n}\n\n/**\n * OpenAI-backed implementation of `ModelContract`.\n *\n * **Role.** The provider-facing bridge between the vendor-neutral\n * `@warlock.js/ai` agent runtime and the official `openai` SDK. Agents,\n * workflows, and supervisors never talk to OpenAI directly — they hold a\n * `ModelContract`, and this class is what makes that contract concrete for\n * any OpenAI-compatible endpoint (OpenAI, Azure OpenAI, OpenRouter, local\n * gateways that speak the Chat Completions protocol).\n *\n * **Responsibility.**\n * - Owns: a long-lived `OpenAI` client + frozen `ModelConfig` (name,\n * temperature, maxTokens) used as defaults for every call.\n * - Owns: translating vendor-neutral `Message[]` and\n * `ToolContract[]` into OpenAI wire shapes on the way out, and\n * translating OpenAI's response (content, finish reason, tool calls,\n * usage) back into the neutral shapes on the way in.\n * - Does NOT own: dispatching tools, deciding whether to loop, tracking\n * conversation history, or retrying on failure — those are agent\n * concerns. The model is a stateless (per-call) protocol adapter.\n *\n * Because it holds a live client and shared defaults, it is modeled as a\n * class (see §4.2 of code-style.md — \"long-lived state across calls\").\n *\n * @example\n * import OpenAI from \"openai\";\n * const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });\n * const model = new OpenAIModel(client, { name: \"gpt-4o\", temperature: 0.3 });\n *\n * const myAgent = agent({\n * model,\n * systemPrompt: \"You are a helpful assistant.\",\n * tools: [searchTool],\n * });\n *\n * const result = await myAgent.execute(\"Summarize today's news.\");\n */\nexport class OpenAIModel implements ModelContract {\n public readonly name: string;\n public readonly provider: string;\n public readonly capabilities: ModelCapabilities;\n public readonly pricing?: ModelPricing;\n\n private readonly client: OpenAI;\n private readonly config: OpenAIModelConfig;\n private readonly logger: Logger = log;\n\n public constructor(client: OpenAI, config: OpenAIModelConfig, provider: string = \"openai\") {\n this.client = client;\n this.config = config;\n this.name = config.name;\n this.provider = provider;\n this.pricing = config.pricing;\n this.capabilities = {\n structuredOutput: config.structuredOutput ?? inferStructuredOutput(config.responseFormat),\n vision: config.vision ?? inferVisionCapability(config.name),\n // o-series + gpt-5 models surface a reasoning channel and accept\n // the `reasoning_effort` param. Explicit config wins over the\n // name-prefix inference.\n reasoning: config.reasoning ?? inferReasoningCapability(config.name),\n // OpenAI prompt caching is automatic on the Chat Completions API\n // (no caller-supplied breakpoints — the platform caches long\n // prompt prefixes server-side and reports the hit count via\n // `prompt_tokens_details.cached_tokens`). We therefore advertise\n // the read-side accounting capability as always available while\n // treating `ModelCallOptions.cacheControl` write breakpoints as a\n // no-op (see `buildReasoningParams` siblings — there is no cache\n // param to emit).\n promptCaching: true,\n };\n }\n\n /**\n * Single-shot completion. Sends the full message list to the Chat\n * Completions endpoint, waits for the terminal response, and reshapes it\n * into a vendor-neutral `ModelResponse`. Per-call `options` override the\n * instance's `ModelConfig` defaults for this call only.\n */\n public async complete(messages: Message[], options?: ModelCallOptions): Promise<ModelResponse> {\n // Per-call request/response logs are hot-path in production agents\n // — keep them at `debug` so `info` stays reserved for lifecycle\n // events (agent starting/completed, etc.). Operators who need to\n // audit every LLM call can raise log-level at runtime.\n this.logger.debug(LOG_MODULE, \"request\", \"Starting call to chat.completions\", {\n model: this.name,\n messageCount: messages.length,\n streaming: false,\n toolCount: options?.tools?.length ?? 0,\n });\n\n let response: OpenAI.Chat.Completions.ChatCompletion;\n\n try {\n response = await this.client.chat.completions.create(\n {\n model: this.name,\n messages: toOpenAIMessages(messages),\n temperature: options?.temperature ?? this.config.temperature,\n max_tokens: options?.maxTokens ?? this.config.maxTokens,\n tools: toOpenAITools(options?.tools),\n ...this.buildResponseFormat(options?.responseSchema),\n ...this.buildReasoningParams(options?.reasoning),\n },\n options?.signal ? { signal: options.signal } : undefined,\n );\n } catch (thrown) {\n const wrapped = wrapOpenAIError(thrown);\n\n this.logger.error(LOG_MODULE, \"error\", wrapped.message, {\n code: wrapped.code,\n context: wrapped.context,\n });\n\n throw wrapped;\n }\n\n const choice = response.choices[0];\n const finishReason = mapFinishReason(choice.finish_reason);\n const usage = this.extractUsage(response.usage);\n\n this.logger.debug(LOG_MODULE, \"response\", \"call to chat.completions succeeded\", {\n finishReason,\n usage,\n });\n\n return {\n content: choice.message.content ?? \"\",\n finishReason,\n usage,\n toolCalls: this.extractToolCalls(choice.message.tool_calls),\n };\n }\n\n /**\n * Incremental streaming completion. Yields neutral `ModelStreamChunk`s —\n * `delta` for text tokens, `tool-call` when the model requests a tool,\n * and a terminal `done` carrying the final finish reason + usage totals.\n * Callers consume it with `for await`.\n */\n public async *stream(\n messages: Message[],\n options?: ModelCallOptions,\n ): AsyncIterable<ModelStreamChunk> {\n this.logger.debug(LOG_MODULE, \"request\", \"Starting streaming call to chat.completions\", {\n model: this.name,\n messageCount: messages.length,\n streaming: true,\n toolCount: options?.tools?.length ?? 0,\n });\n\n let stream: Awaited<ReturnType<typeof this.client.chat.completions.create>>;\n\n try {\n stream = await this.client.chat.completions.create(\n {\n model: this.name,\n messages: toOpenAIMessages(messages),\n temperature: options?.temperature ?? this.config.temperature,\n max_tokens: options?.maxTokens ?? this.config.maxTokens,\n tools: toOpenAITools(options?.tools),\n stream: true,\n stream_options: { include_usage: true },\n ...this.buildResponseFormat(options?.responseSchema),\n ...this.buildReasoningParams(options?.reasoning),\n },\n options?.signal ? { signal: options.signal } : undefined,\n );\n } catch (thrown) {\n const wrapped = wrapOpenAIError(thrown);\n\n this.logger.error(LOG_MODULE, \"error\", wrapped.message, {\n code: wrapped.code,\n context: wrapped.context,\n });\n\n throw wrapped;\n }\n\n let rawFinishReason: string = \"stop\";\n const usage: Usage = { input: 0, output: 0, total: 0 };\n const toolCallAccum = new Map<number, { id: string; name: string; arguments: string }>();\n\n try {\n for await (const chunk of stream as AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>) {\n const delta = chunk.choices[0]?.delta;\n const finish = chunk.choices[0]?.finish_reason;\n\n if (delta?.content) {\n yield { type: \"delta\", content: delta.content };\n }\n\n if (delta?.tool_calls) {\n for (const toolCall of delta.tool_calls) {\n const idx = toolCall.index ?? 0;\n if (!toolCallAccum.has(idx)) {\n toolCallAccum.set(idx, { id: \"\", name: \"\", arguments: \"\" });\n }\n const acc = toolCallAccum.get(idx)!;\n if (toolCall.id) acc.id = toolCall.id;\n if (toolCall.function?.name) acc.name = toolCall.function.name;\n if (toolCall.function?.arguments) acc.arguments += toolCall.function.arguments;\n }\n }\n\n if (finish) {\n rawFinishReason = finish;\n }\n\n if (chunk.usage) {\n usage.input = chunk.usage.prompt_tokens ?? 0;\n usage.output = chunk.usage.completion_tokens ?? 0;\n usage.total = chunk.usage.total_tokens ?? 0;\n const cached = chunk.usage.prompt_tokens_details?.cached_tokens;\n if (cached !== undefined && cached > 0) {\n usage.cachedTokens = cached;\n }\n const reasoning = chunk.usage.completion_tokens_details?.reasoning_tokens;\n if (reasoning !== undefined && reasoning > 0) {\n usage.reasoningTokens = reasoning;\n }\n }\n }\n\n for (const acc of toolCallAccum.values()) {\n // Skip accumulators that never received a function name — those\n // are partial fragments the model started but never identified\n // (e.g. arguments-only deltas with no originating `id`/`name`).\n // Yielding them produces nameless tool-calls the agent runtime\n // can't dispatch and would mis-attribute as a registered tool.\n if (!acc.name) continue;\n\n yield {\n type: \"tool-call\",\n id: acc.id,\n name: acc.name,\n input: safeJsonParse<Record<string, unknown>>(acc.arguments, {}),\n };\n }\n } catch (thrown) {\n const wrapped = wrapOpenAIError(thrown);\n\n this.logger.error(LOG_MODULE, \"error\", wrapped.message, {\n code: wrapped.code,\n context: wrapped.context,\n });\n\n throw wrapped;\n }\n\n const finishReason = mapFinishReason(rawFinishReason);\n\n this.logger.debug(LOG_MODULE, \"response\", \"Streaming call to chat.completions succeeded\", {\n finishReason,\n usage,\n });\n\n yield { type: \"done\", finishReason, usage };\n }\n\n /**\n * Translate the neutral `responseSchema` option into OpenAI's\n * `response_format` parameter.\n *\n * When `config.responseFormat` is set, it wins: `\"text\"` emits no\n * `response_format` at all, `\"json_object\"` always picks the loose\n * mode, and `\"json_schema\"` picks strict mode (with the same\n * `isStrictCompatible` safety check — a malformed schema still\n * degrades to `json_object` rather than 400). The override exists\n * because some targets (older OpenAI models, OpenRouter routes,\n * Ollama OpenAI-compat) reject strict `json_schema` outright.\n *\n * When the override is omitted, uses strict `json_schema` mode\n * (token-level enforcement) only when the schema is a proper\n * root-object JSON Schema (`{ type: \"object\", properties: ... }`).\n * For anything else — malformed extractor output, non-object\n * schemas, or future shapes we haven't tested — falls back to loose\n * `json_object` mode, which guarantees *some* valid JSON without\n * enforcing shape. The agent's soft instruction already embeds the\n * schema text in the system prompt when the model declares no\n * native structured-output capability, so shape validation still\n * runs client-side via the Standard Schema `validate()` call.\n *\n * Returns an empty spread when no schema was supplied, so the caller\n * can unconditionally `...buildResponseFormat(...)` into the request.\n */\n private buildResponseFormat(responseSchema: Record<string, unknown> | undefined): {\n response_format?: OpenAI.Chat.Completions.ChatCompletionCreateParams[\"response_format\"];\n } {\n if (!responseSchema) {\n return {};\n }\n\n const override = this.config.responseFormat;\n\n if (override === \"text\") {\n return {};\n }\n\n if (override === \"json_object\") {\n return { response_format: { type: \"json_object\" } };\n }\n\n // Either auto-select (no override) or explicit `\"json_schema\"`.\n // The strict-compat check still applies in the explicit case —\n // a malformed / non-object schema would 400 before sampling, so\n // we degrade to `json_object` rather than crash.\n if (this.isStrictCompatible(responseSchema)) {\n return {\n response_format: {\n type: \"json_schema\",\n json_schema: {\n name: \"response\",\n schema: responseSchema,\n strict: true,\n },\n },\n };\n }\n\n return { response_format: { type: \"json_object\" } };\n }\n\n /**\n * OpenAI strict `json_schema` mode requires the root to be a JSON\n * Schema object type (`{ type: \"object\", properties: ... }`). Anything\n * else (top-level arrays, primitives, unknown shapes) is rejected with\n * a 400 before a token is sampled. We check structurally here so the\n * first call doesn't crash on a malformed extraction — loose\n * `json_object` mode is a safe degradation.\n */\n private isStrictCompatible(schema: Record<string, unknown>): boolean {\n return (\n schema.type === \"object\" &&\n typeof schema.properties === \"object\" &&\n schema.properties !== null &&\n this.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\n * as nullable, e.g. `type: [\"string\", \"null\"]`, and still appear in\n * `required`). A schema that violates this anywhere in the tree is NOT\n * sent in strict `json_schema` mode — it degrades to loose\n * `json_object` so a hand-built or optional-bearing schema can't 400\n * the call (\"'required' ... must include every key in properties\").\n * Client-side `validate()` still enforces the full shape.\n */\n private 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 (!this.isStrictSafeNode(properties[key])) {\n return false;\n }\n }\n }\n\n if (record.items !== undefined && !this.isStrictSafeNode(record.items)) {\n return false;\n }\n\n for (const branch of [\"anyOf\", \"allOf\", \"oneOf\"] as const) {\n const value = record[branch];\n if (Array.isArray(value) && value.some((sub) => !this.isStrictSafeNode(sub))) {\n return false;\n }\n }\n\n return true;\n }\n\n /**\n * Normalize OpenAI's `usage` block (which may be absent on some responses\n * or partials) into the neutral `Usage` shape. Missing usage collapses to\n * zeros rather than propagating `undefined`, so downstream aggregation\n * math stays safe.\n *\n * `cachedTokens` mirrors `prompt_tokens_details.cached_tokens` (the\n * subset of the prompt served from OpenAI's automatic prompt cache);\n * `reasoningTokens` mirrors `completion_tokens_details.reasoning_tokens`\n * (the hidden reasoning channel on o-series / gpt-5 models, already\n * counted within `output`). Both are emitted only when the provider\n * reports a positive value, so non-reasoning / uncached calls keep the\n * lean `{ input, output, total }` shape.\n */\n private extractUsage(raw: OpenAI.Completions.CompletionUsage | undefined): Usage {\n if (!raw) {\n return { input: 0, output: 0, total: 0 };\n }\n\n const cachedTokens = raw.prompt_tokens_details?.cached_tokens;\n const reasoningTokens = raw.completion_tokens_details?.reasoning_tokens;\n\n return {\n input: raw.prompt_tokens,\n output: raw.completion_tokens,\n total: raw.total_tokens,\n ...(cachedTokens !== undefined && cachedTokens > 0 ? { cachedTokens } : {}),\n ...(reasoningTokens !== undefined && reasoningTokens > 0 ? { reasoningTokens } : {}),\n };\n }\n\n /**\n * Translate the neutral `ModelCallOptions.reasoning` hint into OpenAI's\n * `reasoning_effort` request param. Only `effort` maps — OpenAI's Chat\n * Completions API exposes a discrete effort knob, not a token budget,\n * so `reasoning.maxTokens` (the Anthropic extended-thinking cap) has no\n * wire equivalent here and is silently ignored.\n *\n * No-ops in two cases so the adapter never forwards an unsupported\n * param: (1) the model is not reasoning-capable\n * (`capabilities.reasoning` is false — e.g. `gpt-4o`), or (2) the caller\n * supplied no `effort`. The neutral `ReasoningEffort`\n * (`\"low\" | \"medium\" | \"high\"`) is a strict subset of OpenAI's accepted\n * values, so it forwards verbatim.\n *\n * Returns an empty spread when nothing applies, so the caller can\n * unconditionally `...buildReasoningParams(...)` into the request.\n */\n private buildReasoningParams(reasoning: ModelCallOptions[\"reasoning\"]): {\n reasoning_effort?: OpenAI.Chat.Completions.ChatCompletionCreateParams[\"reasoning_effort\"];\n } {\n if (!this.capabilities.reasoning || !reasoning?.effort) {\n return {};\n }\n\n return { reasoning_effort: reasoning.effort };\n }\n\n /**\n * Reshape OpenAI's `tool_calls` array into the neutral\n * `ModelToolCallRequest[]`. The raw `arguments` field is a JSON string\n * per OpenAI's protocol — we parse it defensively via `safeJsonParse` so\n * malformed or empty arguments yield an empty object instead of crashing\n * the trip. Returns `undefined` when no tools were requested so callers\n * can branch on presence.\n */\n private extractToolCalls(\n rawToolCalls: OpenAI.Chat.Completions.ChatCompletionMessageToolCall[] | undefined,\n ): ModelToolCallRequest[] | undefined {\n if (!rawToolCalls || rawToolCalls.length === 0) {\n return undefined;\n }\n\n return rawToolCalls.map((toolCall) => ({\n id: toolCall.id,\n name: (toolCall as any).function.name,\n input: safeJsonParse<Record<string, unknown>>((toolCall as any).function.arguments, {}),\n }));\n }\n}\n"],"mappings":"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"}
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?, structuredOutput?, responseFormat?}) for ModelContract, .embedder({name, dimensions?}) for embeddings. Triggers: `OpenAISDK`, `.model`, `.embedder`, `.embed`, `.embedMany`, `baseURL`, `pricing`, `responseSchema`, `responseFormat`; "wire openai into a warlock agent", "configure gpt-4o", "route through openrouter or azure openai", "openai embeddings with warlock"; typical import `import { OpenAISDK } from "@warlock.js/ai-openai"`. Skip: 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?}) for ModelContract, .embedder({name, dimensions?}) for embeddings. Triggers: `OpenAISDK`, `.model`, `.embedder`, `.embed`, `.embedMany`, `baseURL`, `pricing`, `responseSchema`, `responseFormat`, `reasoning_effort`, `reasoningTokens`, `cachedTokens`, 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"; typical import `import { OpenAISDK } from "@warlock.js/ai-openai"`. Skip: 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`
@@ -52,8 +52,10 @@ Returns a `ModelContract` you pass straight into `ai.agent({ model })`.
52
52
  | --- | --- |
53
53
  | `structuredOutput` | `true`, unless `responseFormat` is set to `"json_object"` or `"text"` (loose modes) — then `false`. |
54
54
  | `vision` | Inferred from model name. `true` for `gpt-4o*`, `gpt-4-turbo*`, `gpt-4.1*`, `o1*`, `o3*`, `chatgpt-4o*`; `false` otherwise. |
55
+ | `reasoning` | Inferred from model name. `true` for the o-series (`o1*`, `o3*`, `o4*`) and the `gpt-5*` family; `false` otherwise. Drives whether `reasoning_effort` is forwarded. |
56
+ | `promptCaching` | Always `true`. OpenAI caches long prompt prefixes automatically and reports hits via `usage.cachedTokens` — there are no caller-supplied write breakpoints. |
55
57
 
56
- **Override either flag explicitly** via `.model({ name, vision?, structuredOutput? })` — an explicit value always wins over inference.
58
+ **Override `vision`, `structuredOutput`, or `reasoning` explicitly** via `.model({ name, vision?, structuredOutput?, reasoning? })` — an explicit value always wins over inference.
57
59
 
58
60
  ## Structured output
59
61
 
@@ -99,6 +101,33 @@ const { vectors } = await embedder.embedMany(["doc 1", "doc 2", "doc 3"]);
99
101
  openai.embedder({ name: "text-embedding-3-large", dimensions: 256 });
100
102
  ```
101
103
 
104
+ ## Reasoning (o-series / gpt-5)
105
+
106
+ Reasoning models accept a discrete effort knob. The agent passes it through `ModelCallOptions.reasoning`:
107
+
108
+ ```ts
109
+ const model = openai.model({ name: "o3-mini" }); // reasoning auto-true
110
+ await model.complete(messages, { reasoning: { effort: "high" } }); // → reasoning_effort: "high"
111
+ ```
112
+
113
+ - `reasoning.effort` (`"low" | "medium" | "high"`) maps verbatim to OpenAI's `reasoning_effort` request param.
114
+ - `reasoning.maxTokens` has **no Chat Completions equivalent** (it's the Anthropic extended-thinking budget) and is silently ignored here.
115
+ - When `capabilities.reasoning` is `false` (e.g. `gpt-4o`), the option is dropped — the adapter never forwards `reasoning_effort` to a model that would 400 on it. Pin `reasoning: true` to force it for a custom/fine-tuned reasoning model.
116
+
117
+ ## Token usage — what's reported
118
+
119
+ `usage` on the result carries the neutral channel breakdown, populated from OpenAI's `usage` block:
120
+
121
+ | `Usage` field | OpenAI source | Notes |
122
+ | --- | --- | --- |
123
+ | `input` / `output` / `total` | `prompt_tokens` / `completion_tokens` / `total_tokens` | always present (zeroed if OpenAI omits the block). |
124
+ | `cachedTokens` | `prompt_tokens_details.cached_tokens` | subset of `input` served from OpenAI's automatic prompt cache. Emitted only when > 0. |
125
+ | `reasoningTokens` | `completion_tokens_details.reasoning_tokens` | hidden reasoning channel on o-series / gpt-5 (already counted within `output`). Emitted only when > 0. |
126
+
127
+ Both `cachedTokens` and `reasoningTokens` are omitted (not set to `0`) when the provider reports zero, so non-reasoning / uncached calls keep the lean `{ input, output, total }` shape. Reported identically on `complete()` and the streaming `done` event.
128
+
129
+ `cacheControl` write breakpoints (`ModelCallOptions.cacheControl`) are a **no-op** — OpenAI has no caller-driven cache marker on the Chat Completions wire. The read-side `cachedTokens` accounting above works without any caller action.
130
+
102
131
  ## Token counting
103
132
 
104
133
  ```ts
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?, structuredOutput?, responseFormat?}) for ModelContract, .embedder({name, dimensions?}) for embeddings. Triggers: `OpenAISDK`, `.model`, `.embedder`, `.embed`, `.embedMany`, `baseURL`, `pricing`, `responseSchema`, `responseFormat`; "wire openai into a warlock agent", "configure gpt-4o", "route through openrouter or azure openai", "openai embeddings with warlock"; typical import `import { OpenAISDK } from "@warlock.js/ai-openai"`. Skip: 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?}) for ModelContract, .embedder({name, dimensions?}) for embeddings. Triggers: `OpenAISDK`, `.model`, `.embedder`, `.embed`, `.embedMany`, `baseURL`, `pricing`, `responseSchema`, `responseFormat`, `reasoning_effort`, `reasoningTokens`, `cachedTokens`, 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"; typical import `import { OpenAISDK } from "@warlock.js/ai-openai"`. Skip: 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": "^6.34.0",
17
- "@warlock.js/logger": "4.2.11"
17
+ "@warlock.js/logger": "4.4.0"
18
18
  },
19
19
  "peerDependencies": {
20
- "@warlock.js/ai": "4.2.11"
20
+ "@warlock.js/ai": "4.4.0"
21
21
  },
22
- "version": "4.2.11",
22
+ "version": "4.4.0",
23
23
  "main": "./cjs/index.cjs",
24
24
  "module": "./esm/index.mjs",
25
25
  "types": "./esm/index.d.mts",
package/skills/README.md CHANGED
@@ -6,4 +6,4 @@ Per-task skills. All cross-references use the form `@warlock.js/<pkg>/<skill>/SK
6
6
 
7
7
  ### [`setup-openai/`](./setup-openai/SKILL.md)
8
8
 
9
- Wire @warlock.js/ai-openai — new OpenAISDK({apiKey, baseURL?, provider?, pricing?}) for OpenAI / Azure / OpenRouter, .model({name, vision?, structuredOutput?}) for ModelContract, .embedder({name, dimensions?}) for embeddings. Load when wiring an OpenAI-backed model into a @warlock.js agent or routing via an OpenAI-compatible gateway.
9
+ Wire @warlock.js/ai-openai — new OpenAISDK({apiKey, baseURL?, provider?, pricing?}) for OpenAI / Azure / OpenRouter, .model({name, vision?, reasoning?, structuredOutput?, responseFormat?}) for ModelContract, .embedder({name, dimensions?}) for embeddings. Covers o-series / gpt-5 reasoning (reasoning_effort), automatic prompt caching with cost-truth usage (cachedTokens/reasoningTokens), and the per-model pricing registry. Load when wiring an OpenAI-backed model into a @warlock.js agent or routing via an OpenAI-compatible gateway.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: setup-openai
3
- description: 'Wire @warlock.js/ai-openai — new OpenAISDK({apiKey, baseURL?, provider?, pricing?}) for OpenAI / Azure / OpenRouter, .model({name, vision?, structuredOutput?, responseFormat?}) for ModelContract, .embedder({name, dimensions?}) for embeddings. Triggers: `OpenAISDK`, `.model`, `.embedder`, `.embed`, `.embedMany`, `baseURL`, `pricing`, `responseSchema`, `responseFormat`; "wire openai into a warlock agent", "configure gpt-4o", "route through openrouter or azure openai", "openai embeddings with warlock"; typical import `import { OpenAISDK } from "@warlock.js/ai-openai"`. Skip: 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`.'
3
+ description: 'Wire @warlock.js/ai-openai — new OpenAISDK({apiKey, baseURL?, provider?, pricing?}) for OpenAI / Azure / OpenRouter, .model({name, vision?, reasoning?, structuredOutput?, responseFormat?}) for ModelContract, .embedder({name, dimensions?}) for embeddings. Triggers: `OpenAISDK`, `.model`, `.embedder`, `.embed`, `.embedMany`, `baseURL`, `pricing`, `responseSchema`, `responseFormat`, `reasoning_effort`, `reasoningTokens`, `cachedTokens`, 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"; typical import `import { OpenAISDK } from "@warlock.js/ai-openai"`. Skip: 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`.'
4
4
  ---
5
5
 
6
6
  # `@warlock.js/ai-openai`
@@ -44,8 +44,10 @@ Returns a `ModelContract` you pass straight into `ai.agent({ model })`.
44
44
  | --- | --- |
45
45
  | `structuredOutput` | `true`, unless `responseFormat` is set to `"json_object"` or `"text"` (loose modes) — then `false`. |
46
46
  | `vision` | Inferred from model name. `true` for `gpt-4o*`, `gpt-4-turbo*`, `gpt-4.1*`, `o1*`, `o3*`, `chatgpt-4o*`; `false` otherwise. |
47
+ | `reasoning` | Inferred from model name. `true` for the o-series (`o1*`, `o3*`, `o4*`) and the `gpt-5*` family; `false` otherwise. Drives whether `reasoning_effort` is forwarded. |
48
+ | `promptCaching` | Always `true`. OpenAI caches long prompt prefixes automatically and reports hits via `usage.cachedTokens` — there are no caller-supplied write breakpoints. |
47
49
 
48
- **Override either flag explicitly** via `.model({ name, vision?, structuredOutput? })` — an explicit value always wins over inference.
50
+ **Override `vision`, `structuredOutput`, or `reasoning` explicitly** via `.model({ name, vision?, structuredOutput?, reasoning? })` — an explicit value always wins over inference.
49
51
 
50
52
  ## Structured output
51
53
 
@@ -91,6 +93,33 @@ const { vectors } = await embedder.embedMany(["doc 1", "doc 2", "doc 3"]);
91
93
  openai.embedder({ name: "text-embedding-3-large", dimensions: 256 });
92
94
  ```
93
95
 
96
+ ## Reasoning (o-series / gpt-5)
97
+
98
+ Reasoning models accept a discrete effort knob. The agent passes it through `ModelCallOptions.reasoning`:
99
+
100
+ ```ts
101
+ const model = openai.model({ name: "o3-mini" }); // reasoning auto-true
102
+ await model.complete(messages, { reasoning: { effort: "high" } }); // → reasoning_effort: "high"
103
+ ```
104
+
105
+ - `reasoning.effort` (`"low" | "medium" | "high"`) maps verbatim to OpenAI's `reasoning_effort` request param.
106
+ - `reasoning.maxTokens` has **no Chat Completions equivalent** (it's the Anthropic extended-thinking budget) and is silently ignored here.
107
+ - When `capabilities.reasoning` is `false` (e.g. `gpt-4o`), the option is dropped — the adapter never forwards `reasoning_effort` to a model that would 400 on it. Pin `reasoning: true` to force it for a custom/fine-tuned reasoning model.
108
+
109
+ ## Token usage — what's reported
110
+
111
+ `usage` on the result carries the neutral channel breakdown, populated from OpenAI's `usage` block:
112
+
113
+ | `Usage` field | OpenAI source | Notes |
114
+ | --- | --- | --- |
115
+ | `input` / `output` / `total` | `prompt_tokens` / `completion_tokens` / `total_tokens` | always present (zeroed if OpenAI omits the block). |
116
+ | `cachedTokens` | `prompt_tokens_details.cached_tokens` | subset of `input` served from OpenAI's automatic prompt cache. Emitted only when > 0. |
117
+ | `reasoningTokens` | `completion_tokens_details.reasoning_tokens` | hidden reasoning channel on o-series / gpt-5 (already counted within `output`). Emitted only when > 0. |
118
+
119
+ Both `cachedTokens` and `reasoningTokens` are omitted (not set to `0`) when the provider reports zero, so non-reasoning / uncached calls keep the lean `{ input, output, total }` shape. Reported identically on `complete()` and the streaming `done` event.
120
+
121
+ `cacheControl` write breakpoints (`ModelCallOptions.cacheControl`) are a **no-op** — OpenAI has no caller-driven cache marker on the Chat Completions wire. The read-side `cachedTokens` accounting above works without any caller action.
122
+
94
123
  ## Token counting
95
124
 
96
125
  ```ts