@warlock.js/ai-openai 4.8.2 → 4.9.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 +6 -0
- package/cjs/index.cjs +31 -20
- package/cjs/index.cjs.map +1 -1
- package/esm/config.type.d.mts +1 -1
- package/esm/config.type.d.mts.map +1 -1
- package/esm/embedder.d.mts +1 -1
- package/esm/embedder.d.mts.map +1 -1
- package/esm/embedder.mjs +1 -1
- package/esm/embedder.mjs.map +1 -1
- package/esm/image.d.mts +1 -1
- package/esm/image.d.mts.map +1 -1
- package/esm/image.mjs +1 -1
- package/esm/image.mjs.map +1 -1
- package/esm/known-image-models.d.mts +1 -1
- package/esm/known-image-models.d.mts.map +1 -1
- package/esm/known-image-models.mjs +1 -1
- package/esm/known-image-models.mjs.map +1 -1
- package/esm/known-reasoning-models.mjs +1 -1
- package/esm/known-reasoning-models.mjs.map +1 -1
- package/esm/known-vision-models.mjs +1 -1
- package/esm/known-vision-models.mjs.map +1 -1
- package/esm/model.mjs +19 -8
- package/esm/model.mjs.map +1 -1
- package/esm/sdk.d.mts +1 -1
- package/esm/sdk.d.mts.map +1 -1
- package/esm/sdk.mjs +1 -1
- package/esm/sdk.mjs.map +1 -1
- package/esm/speech.d.mts +1 -1
- package/esm/speech.d.mts.map +1 -1
- package/esm/speech.mjs +1 -1
- package/esm/speech.mjs.map +1 -1
- package/esm/transcription.d.mts +1 -1
- package/esm/transcription.d.mts.map +1 -1
- package/esm/transcription.mjs +1 -1
- package/esm/transcription.mjs.map +1 -1
- package/esm/utils/map-finish-reason.mjs +1 -1
- package/esm/utils/map-finish-reason.mjs.map +1 -1
- package/esm/utils/to-openai-messages.mjs +1 -1
- package/esm/utils/to-openai-messages.mjs.map +1 -1
- package/esm/utils/to-openai-tools.mjs +1 -1
- package/esm/utils/to-openai-tools.mjs.map +1 -1
- package/esm/utils/wrap-openai-error.mjs +1 -1
- package/esm/utils/wrap-openai-error.mjs.map +1 -1
- package/llms-full.txt +13 -6
- package/package.json +3 -3
- package/skills/setup-openai/SKILL.md +13 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transcription.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"transcription.mjs","names":[],"sources":["../../../../../../ai-openai/src/transcription.ts"],"sourcesContent":["import {\n InvalidRequestError,\n type AudioInput,\n type TranscribeOptions,\n type TranscriptionModelContract,\n type TranscriptionModelPricing,\n type TranscriptionResponse,\n type TranscriptionSegment,\n} from \"@warlock.js/ai\";\nimport { log, type Logger } from \"@warlock.js/logger\";\nimport OpenAI, { toFile } from \"openai\";\nimport type { OpenAITranscriptionConfig } from \"./config.type\";\nimport { wrapOpenAIError } from \"./utils\";\n\nconst LOG_MODULE = \"ai.openai\";\n\n/** Model-id prefixes OpenAI exposes through the **Transcription** (STT) API. */\nconst TRANSCRIPTION_MODEL_PREFIXES = [\"whisper\", \"gpt-4o-transcribe\", \"gpt-4o-mini-transcribe\"] as const;\n\n/** True when `name` is a recognized OpenAI speech-to-text model. */\nexport function isOpenAITranscriptionModel(name: string): boolean {\n return TRANSCRIPTION_MODEL_PREFIXES.some((prefix) => name.startsWith(prefix));\n}\n\n/** Defensive view over the response, whose shape varies by `response_format`. */\ntype RawTranscription = {\n text: string;\n duration?: number;\n language?: string;\n segments?: Array<{ text: string; start?: number; end?: number }>;\n usage?: {\n type?: string;\n seconds?: number;\n input_tokens?: number;\n output_tokens?: number;\n total_tokens?: number;\n };\n};\n\n/**\n * OpenAI-backed implementation of `TranscriptionModelContract`\n * (speech-to-text) via `audio.transcriptions.create`. Consumed by the\n * `ai.transcribe()` verb.\n *\n * **Response format.** Defaults to `verbose_json` for `whisper-1` (so\n * the run gets a `duration` + timestamped `segments`) and `json` for\n * the token-metered `gpt-4o-transcribe` family. Base64 audio is wrapped\n * in an uploadable via the SDK's `toFile`.\n *\n * @example\n * const stt = new OpenAITranscriptionModel(client, { name: \"whisper-1\" }, \"openai\");\n * const { text } = await stt.transcribe({ base64, mediaType: \"audio/mpeg\" });\n */\nexport class OpenAITranscriptionModel implements TranscriptionModelContract {\n public readonly name: string;\n public readonly provider: string;\n public readonly pricing?: TranscriptionModelPricing;\n\n private readonly client: OpenAI;\n private readonly logger: Logger = log;\n\n public constructor(\n client: OpenAI,\n config: OpenAITranscriptionConfig,\n provider: string = \"openai\",\n ) {\n if (!isOpenAITranscriptionModel(config.name)) {\n throw new InvalidRequestError(\n `\"${config.name}\" is not a known OpenAI transcription model. ` +\n \"Use a `whisper-1` / `gpt-4o-transcribe` / `gpt-4o-mini-transcribe` model with openai.transcribe({ name }).\",\n );\n }\n\n this.client = client;\n this.name = config.name;\n this.provider = provider;\n this.pricing = config.pricing;\n }\n\n public async transcribe(\n audio: AudioInput,\n options?: TranscribeOptions,\n ): Promise<TranscriptionResponse> {\n const isWhisper = this.name.startsWith(\"whisper\");\n const format = options?.format ?? (isWhisper ? \"verbose_json\" : \"json\");\n\n const file = await toFile(Buffer.from(audio.base64, \"base64\"), audio.filename ?? \"audio\", {\n type: audio.mediaType,\n });\n\n this.logger.debug(LOG_MODULE, \"transcription.request\", \"audio.transcriptions.create\", {\n model: this.name,\n format,\n });\n\n let raw: unknown;\n\n try {\n raw = await this.client.audio.transcriptions.create(\n {\n model: this.name,\n file,\n response_format: format as OpenAI.Audio.TranscriptionCreateParams[\"response_format\"],\n ...(options?.language ? { language: options.language } : {}),\n ...(options?.prompt ? { prompt: options.prompt } : {}),\n } as OpenAI.Audio.TranscriptionCreateParamsNonStreaming,\n options?.signal ? { signal: options.signal } : undefined,\n );\n } catch (thrown) {\n const wrapped = wrapOpenAIError(thrown);\n this.logger.error(LOG_MODULE, \"transcription.error\", wrapped.message, {\n code: wrapped.code,\n context: wrapped.context,\n });\n throw wrapped;\n }\n\n const response = raw as RawTranscription;\n\n const segments: TranscriptionSegment[] | undefined = response.segments?.map((segment) => ({\n text: segment.text,\n ...(segment.start !== undefined ? { start: segment.start } : {}),\n ...(segment.end !== undefined ? { end: segment.end } : {}),\n }));\n\n const durationSeconds =\n response.duration ?? (response.usage?.type === \"duration\" ? response.usage.seconds : undefined);\n\n const usage =\n response.usage?.type === \"tokens\"\n ? {\n input: response.usage.input_tokens ?? 0,\n output: response.usage.output_tokens ?? 0,\n total: response.usage.total_tokens ?? 0,\n }\n : { input: 0, output: 0, total: 0 };\n\n return {\n text: response.text,\n ...(segments && segments.length > 0 ? { segments } : {}),\n ...(durationSeconds !== undefined ? { durationSeconds } : {}),\n usage,\n };\n }\n}\n"],"mappings":";;;;;;;AAcA,MAAM,aAAa;;AAGnB,MAAM,+BAA+B;CAAC;CAAW;CAAqB;AAAwB;;AAG9F,SAAgB,2BAA2B,MAAuB;CAChE,OAAO,6BAA6B,MAAM,WAAW,KAAK,WAAW,MAAM,CAAC;AAC9E;;;;;;;;;;;;;;;AA+BA,IAAa,2BAAb,MAA4E;CAQ1E,AAAO,YACL,QACA,QACA,WAAmB,UACnB;gBANgC;EAOhC,IAAI,CAAC,2BAA2B,OAAO,IAAI,GACzC,MAAM,IAAI,oBACR,IAAI,OAAO,KAAK,8JAElB;EAGF,KAAK,SAAS;EACd,KAAK,OAAO,OAAO;EACnB,KAAK,WAAW;EAChB,KAAK,UAAU,OAAO;CACxB;CAEA,MAAa,WACX,OACA,SACgC;EAChC,MAAM,YAAY,KAAK,KAAK,WAAW,SAAS;EAChD,MAAM,SAAS,SAAS,WAAW,YAAY,iBAAiB;EAEhE,MAAM,OAAO,MAAM,OAAO,OAAO,KAAK,MAAM,QAAQ,QAAQ,GAAG,MAAM,YAAY,SAAS,EACxF,MAAM,MAAM,UACd,CAAC;EAED,KAAK,OAAO,MAAM,YAAY,yBAAyB,+BAA+B;GACpF,OAAO,KAAK;GACZ;EACF,CAAC;EAED,IAAI;EAEJ,IAAI;GACF,MAAM,MAAM,KAAK,OAAO,MAAM,eAAe,OAC3C;IACE,OAAO,KAAK;IACZ;IACA,iBAAiB;IACjB,GAAI,SAAS,WAAW,EAAE,UAAU,QAAQ,SAAS,IAAI,CAAC;IAC1D,GAAI,SAAS,SAAS,EAAE,QAAQ,QAAQ,OAAO,IAAI,CAAC;GACtD,GACA,SAAS,SAAS,EAAE,QAAQ,QAAQ,OAAO,IAAI,MACjD;EACF,SAAS,QAAQ;GACf,MAAM,UAAU,gBAAgB,MAAM;GACtC,KAAK,OAAO,MAAM,YAAY,uBAAuB,QAAQ,SAAS;IACpE,MAAM,QAAQ;IACd,SAAS,QAAQ;GACnB,CAAC;GACD,MAAM;EACR;EAEA,MAAM,WAAW;EAEjB,MAAM,WAA+C,SAAS,UAAU,KAAK,aAAa;GACxF,MAAM,QAAQ;GACd,GAAI,QAAQ,UAAU,SAAY,EAAE,OAAO,QAAQ,MAAM,IAAI,CAAC;GAC9D,GAAI,QAAQ,QAAQ,SAAY,EAAE,KAAK,QAAQ,IAAI,IAAI,CAAC;EAC1D,EAAE;EAEF,MAAM,kBACJ,SAAS,aAAa,SAAS,OAAO,SAAS,aAAa,SAAS,MAAM,UAAU;EAEvF,MAAM,QACJ,SAAS,OAAO,SAAS,WACrB;GACE,OAAO,SAAS,MAAM,gBAAgB;GACtC,QAAQ,SAAS,MAAM,iBAAiB;GACxC,OAAO,SAAS,MAAM,gBAAgB;EACxC,IACA;GAAE,OAAO;GAAG,QAAQ;GAAG,OAAO;EAAE;EAEtC,OAAO;GACL,MAAM,SAAS;GACf,GAAI,YAAY,SAAS,SAAS,IAAI,EAAE,SAAS,IAAI,CAAC;GACtD,GAAI,oBAAoB,SAAY,EAAE,gBAAgB,IAAI,CAAC;GAC3D;EACF;CACF;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"map-finish-reason.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"map-finish-reason.mjs","names":[],"sources":["../../../../../../../ai-openai/src/utils/map-finish-reason.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"],"mappings":";AAEA,MAAM,kBAAgD;CACpD,MAAM;CACN,YAAY;CACZ,QAAQ;AACV;;;;;;;;;;AAWA,SAAgB,gBAAgB,KAA8C;CAC5E,OAAO,gBAAgB,OAAO,OAAO;AACvC"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { InvalidRequestError } from "@warlock.js/ai";
|
|
2
2
|
|
|
3
|
-
//#region
|
|
3
|
+
//#region ../ai-openai/src/utils/to-openai-messages.ts
|
|
4
4
|
/**
|
|
5
5
|
* Convert vendor-neutral Message[] to OpenAI's chat message shape.
|
|
6
6
|
* Handles the `tool` role (requires `tool_call_id`) and assistant messages
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"to-openai-messages.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"to-openai-messages.mjs","names":[],"sources":["../../../../../../../ai-openai/src/utils/to-openai-messages.ts"],"sourcesContent":["import { InvalidRequestError, type ContentPart, type Message } from \"@warlock.js/ai\";\nimport type OpenAI from \"openai\";\n\n/**\n * Convert vendor-neutral Message[] to OpenAI's chat message shape.\n * Handles the `tool` role (requires `tool_call_id`) and assistant messages\n * that carry `toolCalls` from a prior model response.\n *\n * Multipart `content` (a `ContentPart[]`) is mapped into OpenAI's user-message\n * content-parts shape: text becomes `{ type: \"text\", text }`, images become\n * `{ type: \"image_url\", image_url: { url } }` — with base64 sources rendered\n * as `data:` URLs inline.\n *\n * @example\n * const openaiMessages = toOpenAIMessages([\n * { role: \"user\", content: \"Hi\" },\n * { role: \"tool\", toolCallId: \"call_1\", content: '{\"ok\":true}' },\n * ]);\n *\n * @example\n * toOpenAIMessages([\n * { role: \"user\", content: [\n * { type: \"text\", text: \"What is this?\" },\n * { type: \"image\", source: { url: \"https://example.com/cat.jpg\" } },\n * ]},\n * ]);\n */\nexport function toOpenAIMessages(\n messages: Message[],\n): 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\n/**\n * Map a resolved `ContentPart` to an OpenAI chat content part — one\n * branch per modality, each to its real wire shape:\n *\n * - `text` → `{ type: \"text\" }`.\n * - `image` → `{ type: \"image_url\" }` (remote URL, or a `data:` URL for\n * inlined base64 bytes).\n * - `pdf` → `{ type: \"file\", file: { file_data } }` (OpenAI document\n * input; base64 only — there is no remote-URL file source).\n * - `audio` → `{ type: \"input_audio\", input_audio: { data, format } }`\n * (base64 only; `wav` / `mp3` are the only formats OpenAI accepts).\n *\n * PDF and audio reach this point ONLY when the model declared the\n * matching capability (`openai.model({ name, pdf: true })` /\n * `{ audio: true }`) — the agent's modality gate throws upfront\n * otherwise, so capability and behavior stay in lockstep. A remote-URL\n * pdf/audio source raises a typed `InvalidRequestError` here rather\n * than a downstream provider fault.\n */\nfunction toOpenAIContentPart(part: ContentPart): OpenAI.Chat.Completions.ChatCompletionContentPart {\n if (part.type === \"text\") {\n return { type: \"text\", text: part.text };\n }\n\n if (part.type === \"image\") {\n const url =\n \"url\" in part.source\n ? part.source.url\n : `data:${part.source.mediaType};base64,${part.source.base64}`;\n\n return { type: \"image_url\", image_url: { url } };\n }\n\n if (part.type === \"pdf\") {\n if (\"url\" in part.source) {\n throw new InvalidRequestError(\n \"OpenAI chat completions cannot fetch a remote-URL PDF; supply base64 document bytes instead.\",\n );\n }\n\n return {\n type: \"file\",\n file: {\n filename: \"document.pdf\",\n file_data: `data:${part.source.mediaType};base64,${part.source.base64}`,\n },\n };\n }\n\n // Audio — the remaining `ContentPart` variant.\n if (\"url\" in part.source) {\n throw new InvalidRequestError(\n \"OpenAI chat completions cannot fetch remote-URL audio; supply base64 audio bytes instead.\",\n );\n }\n\n return {\n type: \"input_audio\",\n input_audio: {\n data: part.source.base64,\n format: toOpenAIAudioFormat(part.source.mediaType),\n },\n };\n}\n\n/**\n * Narrow a neutral audio media type to the two formats OpenAI's\n * `input_audio` accepts (`wav` / `mp3`). An unsupported type raises a\n * typed `InvalidRequestError` up front rather than a provider 400.\n */\nfunction toOpenAIAudioFormat(mediaType: string): \"wav\" | \"mp3\" {\n if (mediaType === \"audio/wav\" || mediaType === \"audio/x-wav\" || mediaType === \"audio/wave\") {\n return \"wav\";\n }\n\n if (mediaType === \"audio/mp3\" || mediaType === \"audio/mpeg\" || mediaType === \"audio/mpga\") {\n return \"mp3\";\n }\n\n throw new InvalidRequestError(\n `OpenAI input_audio supports only \"wav\" and \"mp3\"; got \"${mediaType}\".`,\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,SAAgB,iBACd,UACsD;CACtD,OAAO,SAAS,KAAK,MAAM;EACzB,IAAI,EAAE,SAAS,QACb,OAAO;GACL,MAAM;GACN,SAAS,iBAAiB,EAAE,OAAO;GACnC,cAAc,EAAE,cAAc;EAChC;EAEF,IAAI,EAAE,SAAS,eAAe,EAAE,aAAa,EAAE,UAAU,SAAS,GAChE,OAAO;GACL,MAAM;GACN,SAAS,iBAAiB,EAAE,OAAO;GACnC,YAAY,EAAE,UAAU,KAAK,QAAQ;IACnC,IAAI,GAAG;IACP,MAAM;IACN,UAAU;KAAE,MAAM,GAAG;KAAM,WAAW,KAAK,UAAU,GAAG,SAAS,CAAC,CAAC;IAAE;GACvE,EAAE;EACJ;EAGF,IAAI,EAAE,SAAS,UAAU,MAAM,QAAQ,EAAE,OAAO,GAC9C,OAAO;GACL,MAAM;GACN,SAAS,EAAE,QAAQ,IAAI,mBAAmB;EAC5C;EAGF,OAAO;GAAE,MAAM,EAAE;GAAM,SAAS,iBAAiB,EAAE,OAAO;EAAE;CAI9D,CAAC;AACH;;;;;;;AAQA,SAAS,iBAAiB,SAAyC;CACjE,IAAI,OAAO,YAAY,UACrB,OAAO;CAGT,OAAO,QACJ,QAAQ,SAAiD,KAAK,SAAS,MAAM,CAAC,CAC9E,KAAK,SAAS,KAAK,IAAI,CAAC,CACxB,KAAK,EAAE;AACZ;;;;;;;;;;;;;;;;;;;;AAqBA,SAAS,oBAAoB,MAAsE;CACjG,IAAI,KAAK,SAAS,QAChB,OAAO;EAAE,MAAM;EAAQ,MAAM,KAAK;CAAK;CAGzC,IAAI,KAAK,SAAS,SAMhB,OAAO;EAAE,MAAM;EAAa,WAAW,EAAE,KAJvC,SAAS,KAAK,SACV,KAAK,OAAO,MACZ,QAAQ,KAAK,OAAO,UAAU,UAAU,KAAK,OAAO,SAEb;CAAE;CAGjD,IAAI,KAAK,SAAS,OAAO;EACvB,IAAI,SAAS,KAAK,QAChB,MAAM,IAAI,oBACR,8FACF;EAGF,OAAO;GACL,MAAM;GACN,MAAM;IACJ,UAAU;IACV,WAAW,QAAQ,KAAK,OAAO,UAAU,UAAU,KAAK,OAAO;GACjE;EACF;CACF;CAGA,IAAI,SAAS,KAAK,QAChB,MAAM,IAAI,oBACR,2FACF;CAGF,OAAO;EACL,MAAM;EACN,aAAa;GACX,MAAM,KAAK,OAAO;GAClB,QAAQ,oBAAoB,KAAK,OAAO,SAAS;EACnD;CACF;AACF;;;;;;AAOA,SAAS,oBAAoB,WAAkC;CAC7D,IAAI,cAAc,eAAe,cAAc,iBAAiB,cAAc,cAC5E,OAAO;CAGT,IAAI,cAAc,eAAe,cAAc,gBAAgB,cAAc,cAC3E,OAAO;CAGT,MAAM,IAAI,oBACR,0DAA0D,UAAU,GACtE;AACF"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { extractJsonSchema } from "@warlock.js/ai";
|
|
2
2
|
|
|
3
|
-
//#region
|
|
3
|
+
//#region ../ai-openai/src/utils/to-openai-tools.ts
|
|
4
4
|
/**
|
|
5
5
|
* Convert vendor-neutral ToolConfig[] to OpenAI's tools array.
|
|
6
6
|
* Uses the shared `extractJsonSchema` helper; falls back to an empty-object
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"to-openai-tools.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"to-openai-tools.mjs","names":[],"sources":["../../../../../../../ai-openai/src/utils/to-openai-tools.ts"],"sourcesContent":["import { extractJsonSchema, type ToolConfig } from \"@warlock.js/ai\";\nimport type OpenAI from \"openai\";\n\n/**\n * Convert vendor-neutral ToolConfig[] to OpenAI's tools array.\n * Uses the shared `extractJsonSchema` helper; falls back to an empty-object\n * schema when extraction fails so the tool still registers with the provider.\n *\n * @example\n * const tools = toOpenAITools([weatherTool, calculatorTool]);\n * await client.chat.completions.create({ model, messages, tools });\n */\nexport function toOpenAITools(\n tools: ToolConfig<unknown, unknown>[] | undefined,\n): OpenAI.Chat.Completions.ChatCompletionTool[] | undefined {\n if (!tools || tools.length === 0) {\n return undefined;\n }\n\n return tools.map((tool) => ({\n type: \"function\",\n function: {\n name: tool.name,\n description: tool.description,\n parameters: toParameters(tool.input),\n },\n }));\n}\n\n/**\n * Resolve a tool's input schema to a JSON-Schema object. OpenAI's\n * function `parameters` expects an object root; anything else (or a\n * failed extraction) degrades to an empty-object schema so the tool\n * still registers and the model simply sees no parameters.\n */\nfunction toParameters(input: ToolConfig<unknown, unknown>[\"input\"]): Record<string, unknown> {\n const schema = extractJsonSchema(input);\n\n if (schema && schema.type === \"object\") {\n return schema;\n }\n\n return { type: \"object\", properties: {} };\n}\n"],"mappings":";;;;;;;;;;;;AAYA,SAAgB,cACd,OAC0D;CAC1D,IAAI,CAAC,SAAS,MAAM,WAAW,GAC7B;CAGF,OAAO,MAAM,KAAK,UAAU;EAC1B,MAAM;EACN,UAAU;GACR,MAAM,KAAK;GACX,aAAa,KAAK;GAClB,YAAY,aAAa,KAAK,KAAK;EACrC;CACF,EAAE;AACJ;;;;;;;AAQA,SAAS,aAAa,OAAuE;CAC3F,MAAM,SAAS,kBAAkB,KAAK;CAEtC,IAAI,UAAU,OAAO,SAAS,UAC5B,OAAO;CAGT,OAAO;EAAE,MAAM;EAAU,YAAY,CAAC;CAAE;AAC1C"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import OpenAI from "openai";
|
|
2
2
|
import { AIError, ContentFilterError, ContextLengthExceededError, InvalidRequestError, ProviderAuthError, ProviderError, ProviderRateLimitError, ProviderTimeoutError, QuotaExceededError } from "@warlock.js/ai";
|
|
3
3
|
|
|
4
|
-
//#region
|
|
4
|
+
//#region ../ai-openai/src/utils/wrap-openai-error.ts
|
|
5
5
|
/**
|
|
6
6
|
* Wrap any thrown value caught inside the OpenAI adapter into the
|
|
7
7
|
* appropriate `@warlock.js/ai` `AIError` subclass.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wrap-openai-error.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"wrap-openai-error.mjs","names":[],"sources":["../../../../../../../ai-openai/src/utils/wrap-openai-error.ts"],"sourcesContent":["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"],"mappings":"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"}
|
package/llms-full.txt
CHANGED
|
@@ -133,9 +133,9 @@ await model.complete(messages, { reasoning: { effort: "high" } }); // → reaso
|
|
|
133
133
|
- `reasoning.maxTokens` has **no Chat Completions equivalent** (it's the Anthropic extended-thinking budget) and is silently ignored here.
|
|
134
134
|
- 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.
|
|
135
135
|
|
|
136
|
-
### `effort: "none"` — reasoning off, tools on
|
|
136
|
+
### `effort: "none"` — reasoning off, tools on (DEFAULTS automatically)
|
|
137
137
|
|
|
138
|
-
gpt-5 / o-series models **reject function tools** on the Chat Completions API while reasoning is active:
|
|
138
|
+
gpt-5 / o-series models **reject function tools** on the Chat Completions API while reasoning is active — on some model generations this silently degrades to empty replies, on newer ones it's a hard 400:
|
|
139
139
|
|
|
140
140
|
```
|
|
141
141
|
400 — Function tools with reasoning_effort are not supported for <model>
|
|
@@ -143,14 +143,21 @@ in /v1/chat/completions. To use function tools, use /v1/responses or set
|
|
|
143
143
|
reasoning_effort to 'none'.
|
|
144
144
|
```
|
|
145
145
|
|
|
146
|
-
|
|
146
|
+
Since there is no working alternative to `"none"` in that state, the adapter defaults to it automatically: whenever a call is made to a reasoning-capable model WITH `tools` AND the caller supplied no `reasoning.effort` at all, `reasoning_effort: "none"` is emitted for you — no opt-in required.
|
|
147
147
|
|
|
148
148
|
```ts
|
|
149
|
-
const model = openai.model({ name: "gpt-5-mini" });
|
|
150
|
-
await model.complete(messages, {
|
|
149
|
+
const model = openai.model({ name: "gpt-5-mini" }); // reasoning auto-true
|
|
150
|
+
await model.complete(messages, { tools }); // → reasoning_effort: "none", automatically
|
|
151
151
|
```
|
|
152
152
|
|
|
153
|
-
|
|
153
|
+
An explicit `reasoning.effort` always overrides the default, in either direction — pass it yourself if you want a different value (and accept that tools may then be rejected, per the error above, since Chat Completions still can't do reasoning + tools together):
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
await model.complete(messages, { tools, reasoning: { effort: "high" } }); // your choice wins — reasoning stays on, tools may 400
|
|
157
|
+
await model.complete(messages, { tools, reasoning: { effort: "none" } }); // same effect as the default, explicit
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Calling **without** `tools` is unaffected — `reasoning_effort` is still omitted by default in that case (provider default reasoning, nothing to unblock). Trade-off of the `"none"` default: you lose reasoning on that call. For tool-heavy agent work (function calls + good replies rather than deep analysis) this is the right call — it's the difference between empty replies and working ones. When you need reasoning **and** tools together, use the Responses API (planned).
|
|
154
161
|
|
|
155
162
|
## Token usage — what's reported
|
|
156
163
|
|
package/package.json
CHANGED
|
@@ -14,12 +14,12 @@
|
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"openai": "^6.34.0",
|
|
17
|
-
"@warlock.js/logger": "4.
|
|
17
|
+
"@warlock.js/logger": "4.9.0"
|
|
18
18
|
},
|
|
19
19
|
"peerDependencies": {
|
|
20
|
-
"@warlock.js/ai": "4.
|
|
20
|
+
"@warlock.js/ai": "4.9.0"
|
|
21
21
|
},
|
|
22
|
-
"version": "4.
|
|
22
|
+
"version": "4.9.0",
|
|
23
23
|
"main": "./cjs/index.cjs",
|
|
24
24
|
"module": "./esm/index.mjs",
|
|
25
25
|
"types": "./esm/index.d.mts",
|
|
@@ -125,9 +125,9 @@ await model.complete(messages, { reasoning: { effort: "high" } }); // → reaso
|
|
|
125
125
|
- `reasoning.maxTokens` has **no Chat Completions equivalent** (it's the Anthropic extended-thinking budget) and is silently ignored here.
|
|
126
126
|
- 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.
|
|
127
127
|
|
|
128
|
-
### `effort: "none"` — reasoning off, tools on
|
|
128
|
+
### `effort: "none"` — reasoning off, tools on (DEFAULTS automatically)
|
|
129
129
|
|
|
130
|
-
gpt-5 / o-series models **reject function tools** on the Chat Completions API while reasoning is active:
|
|
130
|
+
gpt-5 / o-series models **reject function tools** on the Chat Completions API while reasoning is active — on some model generations this silently degrades to empty replies, on newer ones it's a hard 400:
|
|
131
131
|
|
|
132
132
|
```
|
|
133
133
|
400 — Function tools with reasoning_effort are not supported for <model>
|
|
@@ -135,14 +135,21 @@ in /v1/chat/completions. To use function tools, use /v1/responses or set
|
|
|
135
135
|
reasoning_effort to 'none'.
|
|
136
136
|
```
|
|
137
137
|
|
|
138
|
-
|
|
138
|
+
Since there is no working alternative to `"none"` in that state, the adapter defaults to it automatically: whenever a call is made to a reasoning-capable model WITH `tools` AND the caller supplied no `reasoning.effort` at all, `reasoning_effort: "none"` is emitted for you — no opt-in required.
|
|
139
139
|
|
|
140
140
|
```ts
|
|
141
|
-
const model = openai.model({ name: "gpt-5-mini" });
|
|
142
|
-
await model.complete(messages, {
|
|
141
|
+
const model = openai.model({ name: "gpt-5-mini" }); // reasoning auto-true
|
|
142
|
+
await model.complete(messages, { tools }); // → reasoning_effort: "none", automatically
|
|
143
143
|
```
|
|
144
144
|
|
|
145
|
-
|
|
145
|
+
An explicit `reasoning.effort` always overrides the default, in either direction — pass it yourself if you want a different value (and accept that tools may then be rejected, per the error above, since Chat Completions still can't do reasoning + tools together):
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
await model.complete(messages, { tools, reasoning: { effort: "high" } }); // your choice wins — reasoning stays on, tools may 400
|
|
149
|
+
await model.complete(messages, { tools, reasoning: { effort: "none" } }); // same effect as the default, explicit
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Calling **without** `tools` is unaffected — `reasoning_effort` is still omitted by default in that case (provider default reasoning, nothing to unblock). Trade-off of the `"none"` default: you lose reasoning on that call. For tool-heavy agent work (function calls + good replies rather than deep analysis) this is the right call — it's the difference between empty replies and working ones. When you need reasoning **and** tools together, use the Responses API (planned).
|
|
146
153
|
|
|
147
154
|
## Token usage — what's reported
|
|
148
155
|
|