@haikit/openai 0.0.0-stage → 0.21.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 hai contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,33 @@
1
- # Temporary Holding Version
1
+ # @haikit/openai
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Model adapter for OpenAI models, over the Responses API.
4
+
5
+ ```ts
6
+ import { openai } from "@haikit/openai";
7
+ createHai({ model: openai({ model: "gpt-6-luna", effort: "medium" }), … });
8
+ ```
9
+
10
+ With no `model`, it uses `gpt-6-luna`; pass any other model ID to change it.
11
+ `effort` defaults to `medium`; pass `effort: null` for a model that doesn't
12
+ reason, such as `gpt-4.1`, and no reasoning settings are sent. The key comes
13
+ from `OPENAI_API_KEY` unless you pass `apiKey`.
14
+
15
+ Note it imports only `@haikit/core`, never `@haikit/server`. Adapters implement an
16
+ interface the contract layer declares — that is what keeps the runtime
17
+ swappable, and what lets an app ship its own adapter without forking anything
18
+ (see `examples/*/src/server/scripted.ts`).
19
+
20
+ HaiKIT keeps the history in the Anthropic Messages shape, so this adapter
21
+ translates it into Responses API input on every call, and the reply back. It
22
+ sends the whole history each time with `store: false` rather than chaining
23
+ `previous_response_id`: the conversation lives in your store, not OpenAI's.
24
+ Reasoning items come back encrypted and are kept in the history, so the next
25
+ call can send them back.
26
+
27
+ A tool keeps strict mode only when OpenAI's strict mode accepts its schema:
28
+ every object lists all its properties in `required` and sets
29
+ `additionalProperties: false`. Any other tool is sent with `strict: false`.
30
+
31
+ A conversation begun with this adapter can't be continued with
32
+ `@haikit/anthropic`, or the other way round: each provider's reasoning blocks
33
+ mean nothing to the other.
@@ -0,0 +1,31 @@
1
+ import type { ModelAdapter } from "@haikit/core";
2
+ /**
3
+ * OpenAI model adapter, over the Responses API.
4
+ *
5
+ * Note it imports only hai-core, never hai-server. Adapters implement an
6
+ * interface the contract layer declares; that is what keeps the runtime
7
+ * swappable, and what lets an app ship its own adapter without forking the
8
+ * framework.
9
+ *
10
+ * It sends the whole history on every call with `store: false`, rather than
11
+ * chaining `previous_response_id`: the conversation lives in the app's store,
12
+ * which HaiKIT itself writes into between calls, and one conversation's calls
13
+ * can come from different servers.
14
+ */
15
+ export interface OpenAIOptions {
16
+ /** Any OpenAI model ID. Defaults to `gpt-6-luna`. */
17
+ model?: string;
18
+ maxTokens?: number;
19
+ /**
20
+ * Defaults to `medium`. `null` sends no reasoning settings at all, for a
21
+ * model that doesn't reason and so refuses them.
22
+ */
23
+ effort?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max" | null;
24
+ apiKey?: string;
25
+ baseURL?: string;
26
+ /** Sent as `prompt_cache_key`, to route calls that share a prefix together. */
27
+ promptCacheKey?: string;
28
+ }
29
+ /** The adapter, given a way to make the client: tests pass a fake one. */
30
+ export declare function createAdapter(options: OpenAIOptions, makeClient: () => Promise<any>): ModelAdapter;
31
+ //# sourceMappingURL=adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAA+B,MAAM,cAAc,CAAC;AAG9E;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,aAAa;IAC5B,qDAAqD;IACrD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,GAAG,IAAI,CAAC;IACjF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+EAA+E;IAC/E,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,0EAA0E;AAC1E,wBAAgB,aAAa,CAAC,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC,GAAG,CAAC,GAAG,YAAY,CAmClG"}
@@ -0,0 +1,35 @@
1
+ import { fromOutput, toInput, toTools } from "./mapping.js";
2
+ /** The adapter, given a way to make the client: tests pass a fake one. */
3
+ export function createAdapter(options, makeClient) {
4
+ const model = options.model ?? "gpt-6-luna";
5
+ const effort = options.effort === undefined ? "medium" : options.effort;
6
+ let client = null;
7
+ return {
8
+ id: model,
9
+ async generate(request) {
10
+ if (!client)
11
+ client = await makeClient();
12
+ const stream = client.responses.stream({
13
+ model,
14
+ // Stable prefix: OpenAI caches it on its own, so nothing volatile may
15
+ // go in here.
16
+ instructions: request.system,
17
+ tools: toTools(request.tools),
18
+ input: toInput(request.messages),
19
+ max_output_tokens: options.maxTokens ?? 16000,
20
+ store: false,
21
+ // Encrypted reasoning comes back only when asked for, and a model that
22
+ // doesn't reason refuses both.
23
+ ...(effort === null ? {} : { reasoning: { effort }, include: ["reasoning.encrypted_content"] }),
24
+ ...(options.promptCacheKey ? { prompt_cache_key: options.promptCacheKey } : {}),
25
+ });
26
+ for await (const event of stream) {
27
+ if (event.type === "response.output_text.delta" || event.type === "response.refusal.delta") {
28
+ request.onTextDelta(event.delta);
29
+ }
30
+ }
31
+ return fromOutput(await stream.finalResponse());
32
+ },
33
+ };
34
+ }
35
+ //# sourceMappingURL=adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.js","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AA8B5D,0EAA0E;AAC1E,MAAM,UAAU,aAAa,CAAC,OAAsB,EAAE,UAA8B;IAClF,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,YAAY,CAAC;IAC5C,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC;IACxE,IAAI,MAAM,GAAQ,IAAI,CAAC;IAEvB,OAAO;QACL,EAAE,EAAE,KAAK;QAET,KAAK,CAAC,QAAQ,CAAC,OAAqB;YAClC,IAAI,CAAC,MAAM;gBAAE,MAAM,GAAG,MAAM,UAAU,EAAE,CAAC;YAEzC,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC;gBACrC,KAAK;gBACL,sEAAsE;gBACtE,cAAc;gBACd,YAAY,EAAE,OAAO,CAAC,MAAM;gBAC5B,KAAK,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC;gBAC7B,KAAK,EAAE,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC;gBAChC,iBAAiB,EAAE,OAAO,CAAC,SAAS,IAAI,KAAK;gBAC7C,KAAK,EAAE,KAAK;gBACZ,uEAAuE;gBACvE,+BAA+B;gBAC/B,GAAG,CAAC,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,CAAC,6BAA6B,CAAC,EAAE,CAAC;gBAC/F,GAAG,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,gBAAgB,EAAE,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAChF,CAAC,CAAC;YAEH,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;gBACjC,IAAI,KAAK,CAAC,IAAI,KAAK,4BAA4B,IAAI,KAAK,CAAC,IAAI,KAAK,wBAAwB,EAAE,CAAC;oBAC3F,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;gBACnC,CAAC;YACH,CAAC;YAED,OAAO,UAAU,CAAC,MAAM,MAAM,CAAC,aAAa,EAAE,CAAC,CAAC;QAClD,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,5 @@
1
+ import type { ModelAdapter } from "@haikit/core";
2
+ import { type OpenAIOptions } from "./adapter.js";
3
+ export type { OpenAIOptions };
4
+ export declare function openai(options?: OpenAIOptions): ModelAdapter;
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAiB,KAAK,aAAa,EAAE,MAAM,cAAc,CAAC;AAEjE,YAAY,EAAE,aAAa,EAAE,CAAC;AAE9B,wBAAgB,MAAM,CAAC,OAAO,GAAE,aAAkB,GAAG,YAAY,CAQhE"}
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ import { createAdapter } from "./adapter.js";
2
+ export function openai(options = {}) {
3
+ return createAdapter(options, async () => {
4
+ const { default: OpenAI } = await import("openai");
5
+ return new OpenAI({
6
+ ...(options.apiKey ? { apiKey: options.apiKey } : {}),
7
+ ...(options.baseURL ? { baseURL: options.baseURL } : {}),
8
+ });
9
+ });
10
+ }
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAsB,MAAM,cAAc,CAAC;AAIjE,MAAM,UAAU,MAAM,CAAC,UAAyB,EAAE;IAChD,OAAO,aAAa,CAAC,OAAO,EAAE,KAAK,IAAI,EAAE;QACvC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,CAAC;QACnD,OAAO,IAAI,MAAM,CAAC;YAChB,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACrD,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACzD,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,20 @@
1
+ import type { Message, ModelRequest, ModelResponse } from "@haikit/core";
2
+ /**
3
+ * The history is kept in the Anthropic Messages shape, so this adapter's work
4
+ * is translation: the history into Responses API input items on the way out,
5
+ * and the response's output items back into content blocks on the way in.
6
+ * Everything here is pure, so it can be tested without the network.
7
+ */
8
+ /** A reasoning item kept in the history so the next call can send it back. */
9
+ export declare const REASONING_BLOCK = "openai_reasoning";
10
+ export declare function toInput(messages: readonly Message[]): any[];
11
+ /**
12
+ * Whether OpenAI's strict mode accepts this schema: every object lists all of
13
+ * its properties in `required` and sets `additionalProperties: false`, all the
14
+ * way down. Sending `strict: true` on a schema that doesn't would fail the
15
+ * whole turn, so such a tool goes without strict mode instead.
16
+ */
17
+ export declare function isStrictCompatible(schema: unknown): boolean;
18
+ export declare function toTools(tools: ModelRequest["tools"]): any[];
19
+ export declare function fromOutput(response: any): ModelResponse;
20
+ //# sourceMappingURL=mapping.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mapping.d.ts","sourceRoot":"","sources":["../src/mapping.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAEzE;;;;;GAKG;AAEH,8EAA8E;AAC9E,eAAO,MAAM,eAAe,qBAAqB,CAAC;AAkBlD,wBAAgB,OAAO,CAAC,QAAQ,EAAE,SAAS,OAAO,EAAE,GAAG,GAAG,EAAE,CA2C3D;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CAoB3D;AAED,wBAAgB,OAAO,CAAC,KAAK,EAAE,YAAY,CAAC,OAAO,CAAC,GAAG,GAAG,EAAE,CAS3D;AAYD,wBAAgB,UAAU,CAAC,QAAQ,EAAE,GAAG,GAAG,aAAa,CAiCvD"}
@@ -0,0 +1,169 @@
1
+ /**
2
+ * The history is kept in the Anthropic Messages shape, so this adapter's work
3
+ * is translation: the history into Responses API input items on the way out,
4
+ * and the response's output items back into content blocks on the way in.
5
+ * Everything here is pure, so it can be tested without the network.
6
+ */
7
+ /** A reasoning item kept in the history so the next call can send it back. */
8
+ export const REASONING_BLOCK = "openai_reasoning";
9
+ function refuse(what) {
10
+ throw new Error(`[haikit] openai: cannot send ${what} to OpenAI`);
11
+ }
12
+ /** A `tool_result`'s content, as the single string `function_call_output` takes. */
13
+ function resultText(content) {
14
+ if (content === undefined || content === null)
15
+ return "";
16
+ if (typeof content === "string")
17
+ return content;
18
+ if (Array.isArray(content)) {
19
+ return content
20
+ .map((b) => (b?.type === "text" && typeof b.text === "string" ? b.text : refuse(`a "${b?.type}" block in a tool_result`)))
21
+ .join("\n");
22
+ }
23
+ return refuse("a tool_result whose content is not a string or an array");
24
+ }
25
+ export function toInput(messages) {
26
+ const input = [];
27
+ for (const message of messages) {
28
+ const { role, content } = message;
29
+ if (typeof content === "string") {
30
+ input.push({ role, content });
31
+ continue;
32
+ }
33
+ if (!Array.isArray(content))
34
+ refuse(`a ${role} message whose content is not a string or an array`);
35
+ if (role === "user") {
36
+ // A function call's output must follow the call, so the results go
37
+ // first: the runtime puts notices' text after them in the same message.
38
+ const texts = [];
39
+ for (const block of content) {
40
+ if (block?.type === "tool_result") {
41
+ input.push({ type: "function_call_output", call_id: block.tool_use_id, output: resultText(block.content) });
42
+ }
43
+ else if (block?.type === "text") {
44
+ texts.push({ type: "input_text", text: block.text });
45
+ }
46
+ else {
47
+ refuse(`a "${block?.type}" block in a user message`);
48
+ }
49
+ }
50
+ if (texts.length > 0)
51
+ input.push({ role: "user", content: texts });
52
+ continue;
53
+ }
54
+ for (const block of content) {
55
+ if (block?.type === "text") {
56
+ if (block.text)
57
+ input.push({ role: "assistant", content: block.text });
58
+ }
59
+ else if (block?.type === "tool_use") {
60
+ input.push({ type: "function_call", call_id: block.id, name: block.name, arguments: JSON.stringify(block.input ?? {}) });
61
+ }
62
+ else if (block?.type === REASONING_BLOCK) {
63
+ input.push({ type: "reasoning", id: block.id, summary: block.summary ?? [], encrypted_content: block.encrypted_content });
64
+ }
65
+ else {
66
+ refuse(`a "${block?.type}" block in an assistant message`);
67
+ }
68
+ }
69
+ }
70
+ return input;
71
+ }
72
+ /**
73
+ * Whether OpenAI's strict mode accepts this schema: every object lists all of
74
+ * its properties in `required` and sets `additionalProperties: false`, all the
75
+ * way down. Sending `strict: true` on a schema that doesn't would fail the
76
+ * whole turn, so such a tool goes without strict mode instead.
77
+ */
78
+ export function isStrictCompatible(schema) {
79
+ if (schema === null || typeof schema !== "object")
80
+ return true;
81
+ if (Array.isArray(schema))
82
+ return schema.every(isStrictCompatible);
83
+ const s = schema;
84
+ const isObject = s.type === "object" || (Array.isArray(s.type) && s.type.includes("object")) || "properties" in s;
85
+ if (isObject) {
86
+ if (s.additionalProperties !== false)
87
+ return false;
88
+ const keys = Object.keys(s.properties ?? {});
89
+ const required = Array.isArray(s.required) ? s.required : [];
90
+ if (!keys.every((k) => required.includes(k)))
91
+ return false;
92
+ }
93
+ for (const key of ["properties", "$defs", "definitions"]) {
94
+ if (s[key] && typeof s[key] === "object" && !Object.values(s[key]).every(isStrictCompatible))
95
+ return false;
96
+ }
97
+ for (const key of ["items", "anyOf", "oneOf", "allOf"]) {
98
+ if (s[key] !== undefined && !isStrictCompatible(s[key]))
99
+ return false;
100
+ }
101
+ return true;
102
+ }
103
+ export function toTools(tools) {
104
+ return tools.map((t) => ({
105
+ type: "function",
106
+ name: t.name,
107
+ description: t.description,
108
+ parameters: t.input_schema,
109
+ // Sent either way, so the API's own default never decides.
110
+ strict: Boolean(t.strict) && isStrictCompatible(t.input_schema),
111
+ }));
112
+ }
113
+ /** A call's arguments as an object, or `null` when they aren't a JSON object. */
114
+ function parseArguments(text) {
115
+ try {
116
+ const value = JSON.parse(String(text));
117
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
118
+ }
119
+ catch {
120
+ return null;
121
+ }
122
+ }
123
+ export function fromOutput(response) {
124
+ // The SDK's stream resolves a failed response rather than throwing, and
125
+ // returning it would end the turn as if the model had nothing to say.
126
+ if (response?.status === "failed" || response?.status === "cancelled") {
127
+ const why = response.error?.message ? `: ${response.error.message}` : "";
128
+ throw new Error(`[haikit] openai: the response ${response.status}${why}`);
129
+ }
130
+ // The runtime never runs the calls of an incomplete reply, so a call cut
131
+ // off part way can stand with `{}`. In a complete one it would be run.
132
+ const incomplete = response?.status === "incomplete";
133
+ const content = [];
134
+ for (const item of response?.output ?? []) {
135
+ if (item?.type === "message") {
136
+ for (const part of item.content ?? []) {
137
+ if (part?.type === "output_text")
138
+ content.push({ type: "text", text: part.text });
139
+ else if (part?.type === "refusal")
140
+ content.push({ type: "text", text: part.refusal });
141
+ }
142
+ }
143
+ else if (item?.type === "function_call") {
144
+ const input = parseArguments(item.arguments);
145
+ if (input === null && !incomplete) {
146
+ throw new Error(`[haikit] openai: the model called ${item.name} with arguments that aren't a JSON object`);
147
+ }
148
+ content.push({ type: "tool_use", id: item.call_id, name: item.name, input: input ?? {} });
149
+ }
150
+ else if (item?.type === "reasoning" && item.encrypted_content) {
151
+ // With `store: false`, OpenAI keeps nothing, so a reasoning item can only
152
+ // be sent back with its encrypted content. One without it is dropped.
153
+ content.push({ type: REASONING_BLOCK, id: item.id, summary: item.summary ?? [], encrypted_content: item.encrypted_content });
154
+ }
155
+ }
156
+ return { content, stop_reason: stopReason(response, content) };
157
+ }
158
+ function stopReason(response, content) {
159
+ if (response?.status === "incomplete") {
160
+ const reason = response.incomplete_details?.reason;
161
+ if (reason === "max_output_tokens")
162
+ return "max_tokens";
163
+ if (reason === "content_filter")
164
+ return "refusal";
165
+ return reason ?? "incomplete";
166
+ }
167
+ return content.some((b) => b.type === "tool_use") ? "tool_use" : "end_turn";
168
+ }
169
+ //# sourceMappingURL=mapping.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mapping.js","sourceRoot":"","sources":["../src/mapping.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,53 @@
1
1
  {
2
2
  "name": "@haikit/openai",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.21.0",
4
+ "description": "OpenAI model adapter for haikit. Imports only @haikit/core, never the server runtime.",
5
+ "keywords": [
6
+ "haikit",
7
+ "openai",
8
+ "gpt",
9
+ "llm",
10
+ "agent",
11
+ "agentic-ui",
12
+ "tool-use",
13
+ "ai"
14
+ ],
15
+ "type": "module",
16
+ "main": "./dist/index.js",
17
+ "types": "./dist/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "default": "./dist/index.js"
22
+ },
23
+ "./package.json": "./package.json"
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "src"
28
+ ],
29
+ "sideEffects": false,
30
+ "engines": {
31
+ "node": ">=22"
32
+ },
33
+ "scripts": {
34
+ "prepack": "npm --prefix ../.. run build"
35
+ },
36
+ "dependencies": {
37
+ "@haikit/core": "0.21.0",
38
+ "openai": "^7.28.0"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "license": "MIT",
44
+ "homepage": "https://github.com/wfoxd/haikit/tree/main/packages/openai#readme",
45
+ "bugs": {
46
+ "url": "https://github.com/wfoxd/haikit/issues"
47
+ },
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "git+https://github.com/wfoxd/haikit.git",
51
+ "directory": "packages/openai"
52
+ }
53
+ }
package/src/adapter.ts ADDED
@@ -0,0 +1,68 @@
1
+ import type { ModelAdapter, ModelRequest, ModelResponse } from "@haikit/core";
2
+ import { fromOutput, toInput, toTools } from "./mapping.js";
3
+
4
+ /**
5
+ * OpenAI model adapter, over the Responses API.
6
+ *
7
+ * Note it imports only hai-core, never hai-server. Adapters implement an
8
+ * interface the contract layer declares; that is what keeps the runtime
9
+ * swappable, and what lets an app ship its own adapter without forking the
10
+ * framework.
11
+ *
12
+ * It sends the whole history on every call with `store: false`, rather than
13
+ * chaining `previous_response_id`: the conversation lives in the app's store,
14
+ * which HaiKIT itself writes into between calls, and one conversation's calls
15
+ * can come from different servers.
16
+ */
17
+ export interface OpenAIOptions {
18
+ /** Any OpenAI model ID. Defaults to `gpt-6-luna`. */
19
+ model?: string;
20
+ maxTokens?: number;
21
+ /**
22
+ * Defaults to `medium`. `null` sends no reasoning settings at all, for a
23
+ * model that doesn't reason and so refuses them.
24
+ */
25
+ effort?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max" | null;
26
+ apiKey?: string;
27
+ baseURL?: string;
28
+ /** Sent as `prompt_cache_key`, to route calls that share a prefix together. */
29
+ promptCacheKey?: string;
30
+ }
31
+
32
+ /** The adapter, given a way to make the client: tests pass a fake one. */
33
+ export function createAdapter(options: OpenAIOptions, makeClient: () => Promise<any>): ModelAdapter {
34
+ const model = options.model ?? "gpt-6-luna";
35
+ const effort = options.effort === undefined ? "medium" : options.effort;
36
+ let client: any = null;
37
+
38
+ return {
39
+ id: model,
40
+
41
+ async generate(request: ModelRequest): Promise<ModelResponse> {
42
+ if (!client) client = await makeClient();
43
+
44
+ const stream = client.responses.stream({
45
+ model,
46
+ // Stable prefix: OpenAI caches it on its own, so nothing volatile may
47
+ // go in here.
48
+ instructions: request.system,
49
+ tools: toTools(request.tools),
50
+ input: toInput(request.messages),
51
+ max_output_tokens: options.maxTokens ?? 16000,
52
+ store: false,
53
+ // Encrypted reasoning comes back only when asked for, and a model that
54
+ // doesn't reason refuses both.
55
+ ...(effort === null ? {} : { reasoning: { effort }, include: ["reasoning.encrypted_content"] }),
56
+ ...(options.promptCacheKey ? { prompt_cache_key: options.promptCacheKey } : {}),
57
+ });
58
+
59
+ for await (const event of stream) {
60
+ if (event.type === "response.output_text.delta" || event.type === "response.refusal.delta") {
61
+ request.onTextDelta(event.delta);
62
+ }
63
+ }
64
+
65
+ return fromOutput(await stream.finalResponse());
66
+ },
67
+ };
68
+ }
package/src/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ import type { ModelAdapter } from "@haikit/core";
2
+ import { createAdapter, type OpenAIOptions } from "./adapter.js";
3
+
4
+ export type { OpenAIOptions };
5
+
6
+ export function openai(options: OpenAIOptions = {}): ModelAdapter {
7
+ return createAdapter(options, async () => {
8
+ const { default: OpenAI } = await import("openai");
9
+ return new OpenAI({
10
+ ...(options.apiKey ? { apiKey: options.apiKey } : {}),
11
+ ...(options.baseURL ? { baseURL: options.baseURL } : {}),
12
+ });
13
+ });
14
+ }
package/src/mapping.ts ADDED
@@ -0,0 +1,166 @@
1
+ import type { Message, ModelRequest, ModelResponse } from "@haikit/core";
2
+
3
+ /**
4
+ * The history is kept in the Anthropic Messages shape, so this adapter's work
5
+ * is translation: the history into Responses API input items on the way out,
6
+ * and the response's output items back into content blocks on the way in.
7
+ * Everything here is pure, so it can be tested without the network.
8
+ */
9
+
10
+ /** A reasoning item kept in the history so the next call can send it back. */
11
+ export const REASONING_BLOCK = "openai_reasoning";
12
+
13
+ function refuse(what: string): never {
14
+ throw new Error(`[haikit] openai: cannot send ${what} to OpenAI`);
15
+ }
16
+
17
+ /** A `tool_result`'s content, as the single string `function_call_output` takes. */
18
+ function resultText(content: unknown): string {
19
+ if (content === undefined || content === null) return "";
20
+ if (typeof content === "string") return content;
21
+ if (Array.isArray(content)) {
22
+ return content
23
+ .map((b: any) => (b?.type === "text" && typeof b.text === "string" ? b.text : refuse(`a "${b?.type}" block in a tool_result`)))
24
+ .join("\n");
25
+ }
26
+ return refuse("a tool_result whose content is not a string or an array");
27
+ }
28
+
29
+ export function toInput(messages: readonly Message[]): any[] {
30
+ const input: any[] = [];
31
+
32
+ for (const message of messages) {
33
+ const { role, content } = message;
34
+
35
+ if (typeof content === "string") {
36
+ input.push({ role, content });
37
+ continue;
38
+ }
39
+ if (!Array.isArray(content)) refuse(`a ${role} message whose content is not a string or an array`);
40
+
41
+ if (role === "user") {
42
+ // A function call's output must follow the call, so the results go
43
+ // first: the runtime puts notices' text after them in the same message.
44
+ const texts: any[] = [];
45
+ for (const block of content) {
46
+ if (block?.type === "tool_result") {
47
+ input.push({ type: "function_call_output", call_id: block.tool_use_id, output: resultText(block.content) });
48
+ } else if (block?.type === "text") {
49
+ texts.push({ type: "input_text", text: block.text });
50
+ } else {
51
+ refuse(`a "${block?.type}" block in a user message`);
52
+ }
53
+ }
54
+ if (texts.length > 0) input.push({ role: "user", content: texts });
55
+ continue;
56
+ }
57
+
58
+ for (const block of content) {
59
+ if (block?.type === "text") {
60
+ if (block.text) input.push({ role: "assistant", content: block.text });
61
+ } else if (block?.type === "tool_use") {
62
+ input.push({ type: "function_call", call_id: block.id, name: block.name, arguments: JSON.stringify(block.input ?? {}) });
63
+ } else if (block?.type === REASONING_BLOCK) {
64
+ input.push({ type: "reasoning", id: block.id, summary: block.summary ?? [], encrypted_content: block.encrypted_content });
65
+ } else {
66
+ refuse(`a "${block?.type}" block in an assistant message`);
67
+ }
68
+ }
69
+ }
70
+
71
+ return input;
72
+ }
73
+
74
+ /**
75
+ * Whether OpenAI's strict mode accepts this schema: every object lists all of
76
+ * its properties in `required` and sets `additionalProperties: false`, all the
77
+ * way down. Sending `strict: true` on a schema that doesn't would fail the
78
+ * whole turn, so such a tool goes without strict mode instead.
79
+ */
80
+ export function isStrictCompatible(schema: unknown): boolean {
81
+ if (schema === null || typeof schema !== "object") return true;
82
+ if (Array.isArray(schema)) return schema.every(isStrictCompatible);
83
+ const s = schema as Record<string, any>;
84
+
85
+ const isObject = s.type === "object" || (Array.isArray(s.type) && s.type.includes("object")) || "properties" in s;
86
+ if (isObject) {
87
+ if (s.additionalProperties !== false) return false;
88
+ const keys = Object.keys(s.properties ?? {});
89
+ const required: unknown[] = Array.isArray(s.required) ? s.required : [];
90
+ if (!keys.every((k) => required.includes(k))) return false;
91
+ }
92
+
93
+ for (const key of ["properties", "$defs", "definitions"]) {
94
+ if (s[key] && typeof s[key] === "object" && !Object.values(s[key]).every(isStrictCompatible)) return false;
95
+ }
96
+ for (const key of ["items", "anyOf", "oneOf", "allOf"]) {
97
+ if (s[key] !== undefined && !isStrictCompatible(s[key])) return false;
98
+ }
99
+ return true;
100
+ }
101
+
102
+ export function toTools(tools: ModelRequest["tools"]): any[] {
103
+ return tools.map((t) => ({
104
+ type: "function",
105
+ name: t.name,
106
+ description: t.description,
107
+ parameters: t.input_schema,
108
+ // Sent either way, so the API's own default never decides.
109
+ strict: Boolean(t.strict) && isStrictCompatible(t.input_schema),
110
+ }));
111
+ }
112
+
113
+ /** A call's arguments as an object, or `null` when they aren't a JSON object. */
114
+ function parseArguments(text: unknown): Record<string, unknown> | null {
115
+ try {
116
+ const value = JSON.parse(String(text));
117
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
118
+ } catch {
119
+ return null;
120
+ }
121
+ }
122
+
123
+ export function fromOutput(response: any): ModelResponse {
124
+ // The SDK's stream resolves a failed response rather than throwing, and
125
+ // returning it would end the turn as if the model had nothing to say.
126
+ if (response?.status === "failed" || response?.status === "cancelled") {
127
+ const why = response.error?.message ? `: ${response.error.message}` : "";
128
+ throw new Error(`[haikit] openai: the response ${response.status}${why}`);
129
+ }
130
+
131
+ // The runtime never runs the calls of an incomplete reply, so a call cut
132
+ // off part way can stand with `{}`. In a complete one it would be run.
133
+ const incomplete = response?.status === "incomplete";
134
+ const content: any[] = [];
135
+
136
+ for (const item of response?.output ?? []) {
137
+ if (item?.type === "message") {
138
+ for (const part of item.content ?? []) {
139
+ if (part?.type === "output_text") content.push({ type: "text", text: part.text });
140
+ else if (part?.type === "refusal") content.push({ type: "text", text: part.refusal });
141
+ }
142
+ } else if (item?.type === "function_call") {
143
+ const input = parseArguments(item.arguments);
144
+ if (input === null && !incomplete) {
145
+ throw new Error(`[haikit] openai: the model called ${item.name} with arguments that aren't a JSON object`);
146
+ }
147
+ content.push({ type: "tool_use", id: item.call_id, name: item.name, input: input ?? {} });
148
+ } else if (item?.type === "reasoning" && item.encrypted_content) {
149
+ // With `store: false`, OpenAI keeps nothing, so a reasoning item can only
150
+ // be sent back with its encrypted content. One without it is dropped.
151
+ content.push({ type: REASONING_BLOCK, id: item.id, summary: item.summary ?? [], encrypted_content: item.encrypted_content });
152
+ }
153
+ }
154
+
155
+ return { content, stop_reason: stopReason(response, content) };
156
+ }
157
+
158
+ function stopReason(response: any, content: any[]): string {
159
+ if (response?.status === "incomplete") {
160
+ const reason = response.incomplete_details?.reason;
161
+ if (reason === "max_output_tokens") return "max_tokens";
162
+ if (reason === "content_filter") return "refusal";
163
+ return reason ?? "incomplete";
164
+ }
165
+ return content.some((b) => b.type === "tool_use") ? "tool_use" : "end_turn";
166
+ }