@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.
- package/README.md +99 -0
- package/cjs/index.cjs +838 -0
- package/cjs/index.cjs.map +1 -0
- package/esm/config.type.d.mts +113 -0
- package/esm/config.type.d.mts.map +1 -0
- package/esm/embedder.d.mts +56 -0
- package/esm/embedder.d.mts.map +1 -0
- package/esm/embedder.mjs +105 -0
- package/esm/embedder.mjs.map +1 -0
- package/esm/index.d.mts +4 -0
- package/esm/index.mjs +4 -0
- package/esm/known-vision-models.mjs +42 -0
- package/esm/known-vision-models.mjs.map +1 -0
- package/esm/model.mjs +309 -0
- package/esm/model.mjs.map +1 -0
- package/esm/sdk.d.mts +79 -0
- package/esm/sdk.d.mts.map +1 -0
- package/esm/sdk.mjs +97 -0
- package/esm/sdk.mjs.map +1 -0
- package/esm/utils/index.mjs +6 -0
- package/esm/utils/map-finish-reason.mjs +22 -0
- package/esm/utils/map-finish-reason.mjs.map +1 -0
- package/esm/utils/to-openai-messages.mjs +78 -0
- package/esm/utils/to-openai-messages.mjs.map +1 -0
- package/esm/utils/to-openai-tools.mjs +41 -0
- package/esm/utils/to-openai-tools.mjs.map +1 -0
- package/esm/utils/wrap-openai-error.mjs +147 -0
- package/esm/utils/wrap-openai-error.mjs.map +1 -0
- package/llms-full.txt +145 -0
- package/llms.txt +9 -0
- package/package.json +38 -0
- package/skills/README.md +9 -0
- package/skills/setup-openai/SKILL.md +135 -0
|
@@ -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
|
+
}
|
package/skills/README.md
ADDED
|
@@ -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
|