@warlock.js/ai-openai 4.1.1

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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"to-openai-messages.mjs","names":[],"sources":["../../../../../../@warlock.js/ai-openai/src/utils/to-openai-messages.ts"],"sourcesContent":["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"],"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,EAC7E,KAAK,SAAS,KAAK,IAAI,EACvB,KAAK,EAAE;AACZ;AAEA,SAAS,oBAAoB,MAAsE;CACjG,IAAI,KAAK,SAAS,QAChB,OAAO;EAAE,MAAM;EAAQ,MAAM,KAAK;CAAK;CASzC,OAAO;EAAE,MAAM;EAAa,WAAW,EAAE,KAJvC,SAAS,KAAK,SACV,KAAK,OAAO,MACZ,QAAQ,KAAK,OAAO,UAAU,UAAU,KAAK,OAAO,SAEb;CAAE;AACjD"}
@@ -0,0 +1,41 @@
1
+ import { extractJsonSchema } from "@warlock.js/ai";
2
+
3
+ //#region ../../@warlock.js/ai-openai/src/utils/to-openai-tools.ts
4
+ /**
5
+ * Convert vendor-neutral ToolConfig[] to OpenAI's tools array.
6
+ * Uses the shared `extractJsonSchema` helper; falls back to an empty-object
7
+ * schema when extraction fails so the tool still registers with the provider.
8
+ *
9
+ * @example
10
+ * const tools = toOpenAITools([weatherTool, calculatorTool]);
11
+ * await client.chat.completions.create({ model, messages, tools });
12
+ */
13
+ function toOpenAITools(tools) {
14
+ if (!tools || tools.length === 0) return;
15
+ return tools.map((tool) => ({
16
+ type: "function",
17
+ function: {
18
+ name: tool.name,
19
+ description: tool.description,
20
+ parameters: toParameters(tool.input)
21
+ }
22
+ }));
23
+ }
24
+ /**
25
+ * Resolve a tool's input schema to a JSON-Schema object. OpenAI's
26
+ * function `parameters` expects an object root; anything else (or a
27
+ * failed extraction) degrades to an empty-object schema so the tool
28
+ * still registers and the model simply sees no parameters.
29
+ */
30
+ function toParameters(input) {
31
+ const schema = extractJsonSchema(input);
32
+ if (schema && schema.type === "object") return schema;
33
+ return {
34
+ type: "object",
35
+ properties: {}
36
+ };
37
+ }
38
+
39
+ //#endregion
40
+ export { toOpenAITools };
41
+ //# sourceMappingURL=to-openai-tools.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"to-openai-tools.mjs","names":[],"sources":["../../../../../../@warlock.js/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"}
@@ -0,0 +1,147 @@
1
+ import OpenAI from "openai";
2
+ import { AIError, ContentFilterError, ContextLengthExceededError, InvalidRequestError, ProviderAuthError, ProviderError, ProviderRateLimitError, ProviderTimeoutError, QuotaExceededError } from "@warlock.js/ai";
3
+
4
+ //#region ../../@warlock.js/ai-openai/src/utils/wrap-openai-error.ts
5
+ /**
6
+ * Wrap any thrown value caught inside the OpenAI adapter into the
7
+ * appropriate `@warlock.js/ai` `AIError` subclass.
8
+ *
9
+ * **Dispatch strategy.** Prefers `APIError.code` when present (stable
10
+ * machine identifier across SDK versions), falls back to `status` when
11
+ * `code` is missing (common with proxied deployments that strip the
12
+ * field). Name-based detection (`APIConnectionTimeoutError`) catches
13
+ * transport-layer errors that never produced an HTTP response.
14
+ *
15
+ * `AIError` instances are returned unchanged — callers can pass the
16
+ * error through `try/catch/throw wrap(e)` pipelines without accidental
17
+ * double-wrapping.
18
+ *
19
+ * @example
20
+ * try {
21
+ * return await this.client.chat.completions.create(...);
22
+ * } catch (thrown) {
23
+ * throw wrapOpenAIError(thrown);
24
+ * }
25
+ */
26
+ function wrapOpenAIError(thrown) {
27
+ if (thrown instanceof AIError) return thrown;
28
+ const shape = toShape(thrown);
29
+ const context = buildContext(thrown, shape);
30
+ const message = shape.message ?? (thrown instanceof Error ? thrown.message : String(thrown));
31
+ if (isTimeout(thrown, shape)) return new ProviderTimeoutError(message, {
32
+ cause: thrown,
33
+ context
34
+ });
35
+ if (shape.status === 401 || shape.code === "invalid_api_key") return new ProviderAuthError(message, {
36
+ cause: thrown,
37
+ context
38
+ });
39
+ if (shape.code === "insufficient_quota") return new QuotaExceededError(message, {
40
+ cause: thrown,
41
+ context
42
+ });
43
+ if (shape.status === 429 || shape.code === "rate_limit_exceeded") return new ProviderRateLimitError(message, {
44
+ cause: thrown,
45
+ context,
46
+ retryAfter: parseRetryAfter(shape.headers)
47
+ });
48
+ if (shape.code === "context_length_exceeded") return new ContextLengthExceededError(message, {
49
+ cause: thrown,
50
+ context
51
+ });
52
+ if (shape.code === "content_filter") return new ContentFilterError(message, {
53
+ cause: thrown,
54
+ context,
55
+ reason: message
56
+ });
57
+ if (typeof shape.status === "number" && shape.status >= 400 && shape.status < 500) return new InvalidRequestError(message, {
58
+ cause: thrown,
59
+ context
60
+ });
61
+ return new ProviderError(message, {
62
+ cause: thrown,
63
+ context
64
+ });
65
+ }
66
+ /**
67
+ * Read the raw error shape without depending on `instanceof APIError`
68
+ * — some consumers wrap the SDK, and proxies sometimes strip the
69
+ * prototype chain. Duck-typing on the visible fields is resilient to
70
+ * both.
71
+ */
72
+ function toShape(thrown) {
73
+ if (thrown instanceof OpenAI.APIError) return {
74
+ status: thrown.status,
75
+ code: thrown.code,
76
+ message: thrown.message,
77
+ type: thrown.type,
78
+ headers: thrown.headers,
79
+ name: thrown.name
80
+ };
81
+ if (typeof thrown === "object" && thrown !== null) {
82
+ const raw = thrown;
83
+ return {
84
+ status: typeof raw.status === "number" ? raw.status : void 0,
85
+ code: typeof raw.code === "string" ? raw.code : void 0,
86
+ message: typeof raw.message === "string" ? raw.message : void 0,
87
+ type: typeof raw.type === "string" ? raw.type : void 0,
88
+ headers: typeof raw.headers === "object" && raw.headers !== null ? raw.headers : void 0,
89
+ name: typeof raw.name === "string" ? raw.name : void 0
90
+ };
91
+ }
92
+ return {};
93
+ }
94
+ /**
95
+ * Decide whether the thrown value represents a timeout. OpenAI's SDK
96
+ * throws `APIConnectionTimeoutError` for transport-level timeouts, and
97
+ * Node surfaces `ETIMEDOUT` / `ECONNABORTED` on the lower socket
98
+ * layer. Either signal counts.
99
+ */
100
+ function isTimeout(thrown, shape) {
101
+ if (thrown instanceof OpenAI.APIConnectionTimeoutError) return true;
102
+ if (shape.name === "APIConnectionTimeoutError") return true;
103
+ if (shape.code === "ETIMEDOUT" || shape.code === "ECONNABORTED") return true;
104
+ return false;
105
+ }
106
+ /**
107
+ * Attach the raw diagnostic fields to `error.context` so consumers
108
+ * have everything the provider surfaced without each subclass having
109
+ * to redeclare them. Never includes `cause` — that lives on
110
+ * `error.cause`.
111
+ */
112
+ function buildContext(thrown, shape) {
113
+ const context = {};
114
+ if (shape.status !== void 0) context.status = shape.status;
115
+ if (shape.code) context.code = shape.code;
116
+ if (shape.type) context.type = shape.type;
117
+ const requestId = readRequestId(thrown);
118
+ if (requestId) context.requestId = requestId;
119
+ return context;
120
+ }
121
+ /**
122
+ * OpenAI puts the request id on `APIError.request_id`. Extract
123
+ * defensively — both camel and snake keys exist across SDK versions.
124
+ */
125
+ function readRequestId(thrown) {
126
+ if (typeof thrown !== "object" || thrown === null) return;
127
+ const raw = thrown;
128
+ if (typeof raw.request_id === "string") return raw.request_id;
129
+ if (typeof raw.requestId === "string") return raw.requestId;
130
+ }
131
+ /**
132
+ * Parse the `Retry-After` response header (seconds per HTTP spec)
133
+ * into milliseconds so consumers can feed it straight to `setTimeout`.
134
+ * Returns `undefined` when missing or unparseable.
135
+ */
136
+ function parseRetryAfter(headers) {
137
+ if (!headers) return;
138
+ const raw = headers["retry-after"] ?? headers["Retry-After"];
139
+ if (!raw) return;
140
+ const seconds = Number(raw);
141
+ if (!Number.isFinite(seconds) || seconds < 0) return;
142
+ return Math.round(seconds * 1e3);
143
+ }
144
+
145
+ //#endregion
146
+ export { wrapOpenAIError };
147
+ //# sourceMappingURL=wrap-openai-error.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wrap-openai-error.mjs","names":[],"sources":["../../../../../../@warlock.js/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 ADDED
@@ -0,0 +1,145 @@
1
+ # Warlock AI Openai — full skills
2
+
3
+ > Package: `@warlock.js/ai-openai`
4
+
5
+ > Generated artifact. Concatenates every SKILL.md and reference file under `@warlock.js/ai-openai/skills/`. Re-run `node scripts/generate-llms.mjs` after any change.
6
+
7
+ ## setup-openai `@warlock.js/ai-openai/setup-openai/SKILL.md`
8
+
9
+ ---
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`.'
12
+ ---
13
+
14
+ # `@warlock.js/ai-openai`
15
+
16
+ Provider adapter that turns OpenAI Chat Completions into a vendor-neutral `ModelContract`. Pair with `@warlock.js/ai` for the agent / tool / system-prompt surface.
17
+
18
+ ## Construction
19
+
20
+ ```ts
21
+ import { OpenAISDK } from "@warlock.js/ai-openai";
22
+
23
+ const openai = new OpenAISDK({ apiKey: process.env.OPENAI_API_KEY! });
24
+
25
+ // OpenAI-compatible endpoints (Azure OpenAI, OpenRouter, local gateways).
26
+ // Pass `provider` to label the upstream — flows through to
27
+ // `ModelContract.provider`, `AgentReport.model.provider`, and logs.
28
+ const openrouter = new OpenAISDK({
29
+ apiKey: process.env.OPENROUTER_API_KEY!,
30
+ baseURL: "https://openrouter.ai/api/v1",
31
+ provider: "openrouter",
32
+ });
33
+ ```
34
+
35
+ `OpenAISDK` is a class (not a factory) — adapter entry points hold a long-lived `OpenAI` client and align with `new OpenAI(...)` upstream convention.
36
+
37
+ `provider` defaults to `"openai"`. It's an SDK-level identity (one client = one upstream), not a per-model knob.
38
+
39
+ ## Producing a model
40
+
41
+ ```ts
42
+ openai.model({ name: "gpt-4o-mini" }) // common case
43
+ openai.model({ name: "gpt-4o", temperature: 0.2 }) // sampling controls
44
+ openai.model({ name: "fine-tuned-x", vision: true }) // explicit capability override
45
+ ```
46
+
47
+ Returns a `ModelContract` you pass straight into `ai.agent({ model })`.
48
+
49
+ ## Capabilities — what's auto-set
50
+
51
+ | Flag | Default |
52
+ | --- | --- |
53
+ | `structuredOutput` | `true`, unless `responseFormat` is set to `"json_object"` or `"text"` (loose modes) — then `false`. |
54
+ | `vision` | Inferred from model name. `true` for `gpt-4o*`, `gpt-4-turbo*`, `gpt-4.1*`, `o1*`, `o3*`, `chatgpt-4o*`; `false` otherwise. |
55
+
56
+ **Override either flag explicitly** via `.model({ name, vision?, structuredOutput? })` — an explicit value always wins over inference.
57
+
58
+ ## Structured output
59
+
60
+ When the agent passes `responseSchema` (a JSON Schema object), the adapter converts to `response_format: json_schema, strict: true` on the wire — token-level enforcement. Non-object schemas fall back to loose `json_object` mode (still valid JSON, no shape enforcement).
61
+
62
+ Some targets reject strict `json_schema` (older OpenAI models, OpenRouter routes, Ollama OpenAI-compat). Override the wire mode per model with `responseFormat`:
63
+
64
+ ```ts
65
+ openai.model({ name: "legacy-model", responseFormat: "json_object" }) // valid JSON, no strict shape
66
+ openai.model({ name: "some-route", responseFormat: "text" }) // no response_format on the wire
67
+ ```
68
+
69
+ `"json_object"` and `"text"` also flip `structuredOutput` to `false`, so the agent re-injects the schema as a soft prompt hint. Pin `structuredOutput` explicitly to override that.
70
+
71
+ ## Multipart messages (vision)
72
+
73
+ `ContentPart[]` user content is rendered into OpenAI's content-parts shape:
74
+
75
+ - `{ type: "text", text }` → `{ type: "text", text }`
76
+ - `{ type: "image", source: { url } }` → `{ type: "image_url", image_url: { url } }`
77
+ - `{ type: "image", source: { base64, mediaType } }` → `{ type: "image_url", image_url: { url: "data:{mediaType};base64,{base64}" } }`
78
+
79
+ The agent prepares attachments before they reach the adapter; this package never reads files itself.
80
+
81
+ ## Streaming
82
+
83
+ `model.stream()` drains `chat.completions.create({ stream: true })` and yields `{ type: "delta", content }` per token, then — after the stream closes — one consolidated `{ type: "tool-call", id, name, input }` per requested tool, then a terminal `{ type: "done", finishReason, usage }`. `stream_options: { include_usage: true }` is enabled by default.
84
+
85
+ Tool-call argument fragments arrive split across deltas; the adapter accumulates them per `tool_calls[n].index` and parses the assembled JSON once on completion, so streamed tool calls round-trip identically to non-streaming `complete()`. Accumulators that never received a function name are skipped.
86
+
87
+ ## Embeddings
88
+
89
+ ```ts
90
+ const embedder = openai.embedder({ name: "text-embedding-3-small" });
91
+
92
+ const { vector, dimensions, usage } = await embedder.embed("Hello world");
93
+ const { vectors } = await embedder.embedMany(["doc 1", "doc 2", "doc 3"]);
94
+ ```
95
+
96
+ `dimensions` is lazy — starts at `0`, populated from the first response. Pass `dimensions` in config to request output truncation (supported by `text-embedding-3-*`):
97
+
98
+ ```ts
99
+ openai.embedder({ name: "text-embedding-3-large", dimensions: 256 });
100
+ ```
101
+
102
+ ## Token counting
103
+
104
+ ```ts
105
+ await openai.count("some text") // approximate (heuristic, not tiktoken)
106
+ ```
107
+
108
+ Good enough for budgeting; not for billing.
109
+
110
+ ## Pricing — per-model registry
111
+
112
+ `pricing` is a **registry keyed by model name** — one entry per model, all rates in **USD per 1,000,000 tokens** (`ModelPricing`: `input`, `output`, optional `cachedInput` / `cachedOutput`).
113
+
114
+ ```ts
115
+ const openai = new OpenAISDK({
116
+ apiKey,
117
+ pricing: {
118
+ // USD per 1M tokens.
119
+ "gpt-4o-mini": { input: 0.15, output: 0.6, cachedInput: 0.075 },
120
+ "gpt-4o": { input: 2.5, output: 10, cachedInput: 1.25 },
121
+ },
122
+ });
123
+
124
+ const { usage } = await ai.agent({ model: openai.model({ name: "gpt-4o-mini" }) }).execute("hi");
125
+ usage.cost; // { input, output, cachedInput?, cachedOutput? } — per-channel USD breakdown of THIS run
126
+ ```
127
+
128
+ The registry is per-model; `usage.cost` is the per-channel breakdown the framework computes from `tokens × pricing[model]`. Resolution at `model()` time: per-model `pricing` (`openai.model({ name, pricing })`) > SDK registry > `undefined` (no cost computed). See [`@warlock.js/ai/pick-ai-provider/SKILL.md`](@warlock.js/ai/pick-ai-provider/SKILL.md).
129
+
130
+ ## Errors
131
+
132
+ Raw OpenAI SDK errors are wrapped into the typed `@warlock.js/ai` `AIError` hierarchy via the adapter's error wrapper. Dispatch keys on `APIError.status + code` combined — see [`@warlock.js/ai/handle-ai-errors/SKILL.md`](@warlock.js/ai/handle-ai-errors/SKILL.md).
133
+
134
+ ## When NOT to use this skill
135
+
136
+ - Direct calls to the `openai` SDK without going through `@warlock.js/ai` agents.
137
+ - Anthropic models — use `@warlock.js/ai-anthropic`. Bedrock — `@warlock.js/ai-bedrock`. Gemini — `@warlock.js/ai-google`. Ollama — `@warlock.js/ai-ollama`.
138
+
139
+ ## See also
140
+
141
+ - [`@warlock.js/ai/run-ai-agent/SKILL.md`](@warlock.js/ai/run-ai-agent/SKILL.md) — passing the model into `ai.agent({...})`
142
+ - [`@warlock.js/ai/embed-text/SKILL.md`](@warlock.js/ai/embed-text/SKILL.md) — embedder usage
143
+ - [`@warlock.js/ai/pick-ai-provider/SKILL.md`](@warlock.js/ai/pick-ai-provider/SKILL.md) — adapter comparison
144
+
145
+
package/llms.txt ADDED
@@ -0,0 +1,9 @@
1
+ # Warlock AI Openai
2
+
3
+ > Package: `@warlock.js/ai-openai`
4
+
5
+ > OpenAI SDK adapter for @warlock.js/ai
6
+
7
+ ## Skills
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`.
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@warlock.js/ai-openai",
3
+ "description": "OpenAI SDK adapter for @warlock.js/ai",
4
+ "keywords": [
5
+ "warlock",
6
+ "ai",
7
+ "openai"
8
+ ],
9
+ "author": "Hasan Zohdy",
10
+ "license": "MIT",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/warlockjs/ai-openai"
14
+ },
15
+ "dependencies": {
16
+ "openai": "^6.34.0",
17
+ "@warlock.js/logger": "*"
18
+ },
19
+ "peerDependencies": {
20
+ "@warlock.js/ai": "*"
21
+ },
22
+ "version": "4.1.1",
23
+ "main": "./cjs/index.cjs",
24
+ "module": "./esm/index.mjs",
25
+ "types": "./esm/index.d.mts",
26
+ "exports": {
27
+ ".": {
28
+ "import": {
29
+ "types": "./esm/index.d.mts",
30
+ "default": "./esm/index.mjs"
31
+ },
32
+ "require": {
33
+ "types": "./esm/index.d.mts",
34
+ "default": "./cjs/index.cjs"
35
+ }
36
+ }
37
+ }
38
+ }
@@ -0,0 +1,9 @@
1
+ # `@warlock.js/ai-openai` — skills index
2
+
3
+ Per-task skills. All cross-references use the form `@warlock.js/<pkg>/<skill>/SKILL.md`.
4
+
5
+ ## Skills
6
+
7
+ ### [`setup-openai/`](./setup-openai/SKILL.md)
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.
@@ -0,0 +1,135 @@
1
+ ---
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`.'
4
+ ---
5
+
6
+ # `@warlock.js/ai-openai`
7
+
8
+ Provider adapter that turns OpenAI Chat Completions into a vendor-neutral `ModelContract`. Pair with `@warlock.js/ai` for the agent / tool / system-prompt surface.
9
+
10
+ ## Construction
11
+
12
+ ```ts
13
+ import { OpenAISDK } from "@warlock.js/ai-openai";
14
+
15
+ const openai = new OpenAISDK({ apiKey: process.env.OPENAI_API_KEY! });
16
+
17
+ // OpenAI-compatible endpoints (Azure OpenAI, OpenRouter, local gateways).
18
+ // Pass `provider` to label the upstream — flows through to
19
+ // `ModelContract.provider`, `AgentReport.model.provider`, and logs.
20
+ const openrouter = new OpenAISDK({
21
+ apiKey: process.env.OPENROUTER_API_KEY!,
22
+ baseURL: "https://openrouter.ai/api/v1",
23
+ provider: "openrouter",
24
+ });
25
+ ```
26
+
27
+ `OpenAISDK` is a class (not a factory) — adapter entry points hold a long-lived `OpenAI` client and align with `new OpenAI(...)` upstream convention.
28
+
29
+ `provider` defaults to `"openai"`. It's an SDK-level identity (one client = one upstream), not a per-model knob.
30
+
31
+ ## Producing a model
32
+
33
+ ```ts
34
+ openai.model({ name: "gpt-4o-mini" }) // common case
35
+ openai.model({ name: "gpt-4o", temperature: 0.2 }) // sampling controls
36
+ openai.model({ name: "fine-tuned-x", vision: true }) // explicit capability override
37
+ ```
38
+
39
+ Returns a `ModelContract` you pass straight into `ai.agent({ model })`.
40
+
41
+ ## Capabilities — what's auto-set
42
+
43
+ | Flag | Default |
44
+ | --- | --- |
45
+ | `structuredOutput` | `true`, unless `responseFormat` is set to `"json_object"` or `"text"` (loose modes) — then `false`. |
46
+ | `vision` | Inferred from model name. `true` for `gpt-4o*`, `gpt-4-turbo*`, `gpt-4.1*`, `o1*`, `o3*`, `chatgpt-4o*`; `false` otherwise. |
47
+
48
+ **Override either flag explicitly** via `.model({ name, vision?, structuredOutput? })` — an explicit value always wins over inference.
49
+
50
+ ## Structured output
51
+
52
+ When the agent passes `responseSchema` (a JSON Schema object), the adapter converts to `response_format: json_schema, strict: true` on the wire — token-level enforcement. Non-object schemas fall back to loose `json_object` mode (still valid JSON, no shape enforcement).
53
+
54
+ Some targets reject strict `json_schema` (older OpenAI models, OpenRouter routes, Ollama OpenAI-compat). Override the wire mode per model with `responseFormat`:
55
+
56
+ ```ts
57
+ openai.model({ name: "legacy-model", responseFormat: "json_object" }) // valid JSON, no strict shape
58
+ openai.model({ name: "some-route", responseFormat: "text" }) // no response_format on the wire
59
+ ```
60
+
61
+ `"json_object"` and `"text"` also flip `structuredOutput` to `false`, so the agent re-injects the schema as a soft prompt hint. Pin `structuredOutput` explicitly to override that.
62
+
63
+ ## Multipart messages (vision)
64
+
65
+ `ContentPart[]` user content is rendered into OpenAI's content-parts shape:
66
+
67
+ - `{ type: "text", text }` → `{ type: "text", text }`
68
+ - `{ type: "image", source: { url } }` → `{ type: "image_url", image_url: { url } }`
69
+ - `{ type: "image", source: { base64, mediaType } }` → `{ type: "image_url", image_url: { url: "data:{mediaType};base64,{base64}" } }`
70
+
71
+ The agent prepares attachments before they reach the adapter; this package never reads files itself.
72
+
73
+ ## Streaming
74
+
75
+ `model.stream()` drains `chat.completions.create({ stream: true })` and yields `{ type: "delta", content }` per token, then — after the stream closes — one consolidated `{ type: "tool-call", id, name, input }` per requested tool, then a terminal `{ type: "done", finishReason, usage }`. `stream_options: { include_usage: true }` is enabled by default.
76
+
77
+ Tool-call argument fragments arrive split across deltas; the adapter accumulates them per `tool_calls[n].index` and parses the assembled JSON once on completion, so streamed tool calls round-trip identically to non-streaming `complete()`. Accumulators that never received a function name are skipped.
78
+
79
+ ## Embeddings
80
+
81
+ ```ts
82
+ const embedder = openai.embedder({ name: "text-embedding-3-small" });
83
+
84
+ const { vector, dimensions, usage } = await embedder.embed("Hello world");
85
+ const { vectors } = await embedder.embedMany(["doc 1", "doc 2", "doc 3"]);
86
+ ```
87
+
88
+ `dimensions` is lazy — starts at `0`, populated from the first response. Pass `dimensions` in config to request output truncation (supported by `text-embedding-3-*`):
89
+
90
+ ```ts
91
+ openai.embedder({ name: "text-embedding-3-large", dimensions: 256 });
92
+ ```
93
+
94
+ ## Token counting
95
+
96
+ ```ts
97
+ await openai.count("some text") // approximate (heuristic, not tiktoken)
98
+ ```
99
+
100
+ Good enough for budgeting; not for billing.
101
+
102
+ ## Pricing — per-model registry
103
+
104
+ `pricing` is a **registry keyed by model name** — one entry per model, all rates in **USD per 1,000,000 tokens** (`ModelPricing`: `input`, `output`, optional `cachedInput` / `cachedOutput`).
105
+
106
+ ```ts
107
+ const openai = new OpenAISDK({
108
+ apiKey,
109
+ pricing: {
110
+ // USD per 1M tokens.
111
+ "gpt-4o-mini": { input: 0.15, output: 0.6, cachedInput: 0.075 },
112
+ "gpt-4o": { input: 2.5, output: 10, cachedInput: 1.25 },
113
+ },
114
+ });
115
+
116
+ const { usage } = await ai.agent({ model: openai.model({ name: "gpt-4o-mini" }) }).execute("hi");
117
+ usage.cost; // { input, output, cachedInput?, cachedOutput? } — per-channel USD breakdown of THIS run
118
+ ```
119
+
120
+ The registry is per-model; `usage.cost` is the per-channel breakdown the framework computes from `tokens × pricing[model]`. Resolution at `model()` time: per-model `pricing` (`openai.model({ name, pricing })`) > SDK registry > `undefined` (no cost computed). See [`@warlock.js/ai/pick-ai-provider/SKILL.md`](@warlock.js/ai/pick-ai-provider/SKILL.md).
121
+
122
+ ## Errors
123
+
124
+ Raw OpenAI SDK errors are wrapped into the typed `@warlock.js/ai` `AIError` hierarchy via the adapter's error wrapper. Dispatch keys on `APIError.status + code` combined — see [`@warlock.js/ai/handle-ai-errors/SKILL.md`](@warlock.js/ai/handle-ai-errors/SKILL.md).
125
+
126
+ ## When NOT to use this skill
127
+
128
+ - Direct calls to the `openai` SDK without going through `@warlock.js/ai` agents.
129
+ - Anthropic models — use `@warlock.js/ai-anthropic`. Bedrock — `@warlock.js/ai-bedrock`. Gemini — `@warlock.js/ai-google`. Ollama — `@warlock.js/ai-ollama`.
130
+
131
+ ## See also
132
+
133
+ - [`@warlock.js/ai/run-ai-agent/SKILL.md`](@warlock.js/ai/run-ai-agent/SKILL.md) — passing the model into `ai.agent({...})`
134
+ - [`@warlock.js/ai/embed-text/SKILL.md`](@warlock.js/ai/embed-text/SKILL.md) — embedder usage
135
+ - [`@warlock.js/ai/pick-ai-provider/SKILL.md`](@warlock.js/ai/pick-ai-provider/SKILL.md) — adapter comparison