@vincemakes/kiso-provider-openai-responses 0.31.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 +21 -0
- package/README.md +58 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.js +629 -0
- package/package.json +43 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 kiso 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
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# @vincemakes/kiso-provider-openai-responses
|
|
2
|
+
|
|
3
|
+
The OpenAI **Responses** adapter — one adapter, two targets, no SDK and no
|
|
4
|
+
new dependency (plain `fetch` plus a hand-rolled SSE reader).
|
|
5
|
+
|
|
6
|
+
| target | auth | endpoint | what is different |
|
|
7
|
+
|---|---|---|---|
|
|
8
|
+
| OpenAI first-party | API key (`kiso login openai`, or `OPENAI_API_KEY`) | `https://api.openai.com/v1/responses` | the public Responses API |
|
|
9
|
+
| ChatGPT subscription | the stored OAuth credential (`kiso login chatgpt`) | `https://chatgpt.com/backend-api/codex/responses` | `chatgpt-account-id` / `originator` / `OpenAI-Beta` headers; `store: false`, `include: ["reasoning.encrypted_content"]`, `prompt_cache_key` |
|
|
10
|
+
|
|
11
|
+
The target is inferred from the options the factory is handed — an
|
|
12
|
+
`apiKey` builds the first-party adapter, an `oauth` thunk builds the
|
|
13
|
+
ChatGPT one — so there is no second switch that could disagree with the
|
|
14
|
+
credential. The thunk is awaited **once per request**, which is what lets
|
|
15
|
+
the credential store refresh a token that expires mid-session.
|
|
16
|
+
|
|
17
|
+
Requires Node >= 22. See the repository README for the framework overview.
|
|
18
|
+
|
|
19
|
+
## Support level
|
|
20
|
+
|
|
21
|
+
Filled from this package's own gates. Every one of them runs against a
|
|
22
|
+
LOCAL double (`tests/helpers/rig.ts`); no test reaches a vendor.
|
|
23
|
+
|
|
24
|
+
| capability | first-party | ChatGPT | evidence |
|
|
25
|
+
|---|---|---|---|
|
|
26
|
+
| auth header | ✓ | ✓ | `or1-request-rig` — the frozen request bytes per target, and the ChatGPT-only headers absent on the first-party one |
|
|
27
|
+
| streaming | ✓ | ✓ | `or1-stream` — the nine adapter events in order, `usage` before `stop`, both from the same terminal frame |
|
|
28
|
+
| tool call + next turn | ✓ | ✓ | `or1-tool-turn` — the emitted `callId` is the `call_id`, and the next request replays it as `function_call_output` |
|
|
29
|
+
| reasoning effort | ✓ | ✓ | `or1-request-rig` shape 3 — `reasoning.effort` carries the level it was handed |
|
|
30
|
+
| reasoning replay (`store: false`) | n/a | ✓ | `or1-continuation` — one `stop.continuation` entry per encrypted reasoning item, replayed on scope match only |
|
|
31
|
+
| cancel | ✓ | ✓ | `or1-errors` — an abort ends the turn with an `AbortError`, no `stop`, nothing retryable |
|
|
32
|
+
| error mapping | ✓ | ✓ | `or1-errors` — 429/401/400/503 by status, both `Retry-After` forms in milliseconds |
|
|
33
|
+
| retry authority | ✓ | ✓ | `or1-retry-authority` — exactly one request per stream, on every failure class |
|
|
34
|
+
| **a real vendor leg** | **unrun** | **unrun** | needs a key / a subscription sign-in and an owner-budgeted run |
|
|
35
|
+
|
|
36
|
+
`unrun` means exactly that: the behaviour is proven against the recorded
|
|
37
|
+
dialect, and nobody has yet pointed this adapter at the vendor.
|
|
38
|
+
|
|
39
|
+
## What it deliberately does not send
|
|
40
|
+
|
|
41
|
+
`text.verbosity`, `tool_choice`, `parallel_tool_calls`, `service_tier`,
|
|
42
|
+
zstd request compression, and the WebSocket transport. Each is a real
|
|
43
|
+
field on the wire; none of them has a kiso setting behind it, and a
|
|
44
|
+
hardcoded default would be this adapter inventing a policy nobody chose.
|
|
45
|
+
|
|
46
|
+
`strict` is likewise absent from tool definitions: kiso's schemas are
|
|
47
|
+
already closed worlds validated by the kernel, and the flag would add a
|
|
48
|
+
second validator with different rules.
|
|
49
|
+
|
|
50
|
+
## Two behaviours worth knowing
|
|
51
|
+
|
|
52
|
+
- **`max_output_tokens` below 16 is refused, not clamped.** The provider
|
|
53
|
+
rejects it; kiso answers before the request rather than silently
|
|
54
|
+
raising a bound the caller asked for.
|
|
55
|
+
- **`usage.inputTokens` is the provider's RAW count**, which on this
|
|
56
|
+
dialect includes the cached prefix. The runtime's canonicalizer owns
|
|
57
|
+
the subtraction — doing it here too would bill the cached tokens away
|
|
58
|
+
twice.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The OpenAI Responses adapter — one adapter, two targets.
|
|
3
|
+
*
|
|
4
|
+
* Plain `fetch` plus a hand-rolled SSE reader: no SDK, no new dependency.
|
|
5
|
+
* The Responses dialect is small enough that a vendor client would buy
|
|
6
|
+
* nothing here and would bring its own retry policy, which this tree
|
|
7
|
+
* refuses (the kernel is the sole retry authority — CX-1 F8).
|
|
8
|
+
*
|
|
9
|
+
* The TARGET is inferred from the options the factory is handed, never
|
|
10
|
+
* from a separate switch that could disagree with them:
|
|
11
|
+
* { apiKey } → the first-party Responses API (api.openai.com/v1/responses)
|
|
12
|
+
* { oauth } → the ChatGPT subscription backend
|
|
13
|
+
* (chatgpt.com/backend-api/codex/responses)
|
|
14
|
+
* The `oauth` thunk is called ONCE PER REQUEST, so a token that expires
|
|
15
|
+
* mid-session is refreshed by whoever owns the credential store — the
|
|
16
|
+
* adapter never caches a token and never refreshes one itself.
|
|
17
|
+
*
|
|
18
|
+
* Design source: the reference implementation's client for this backend
|
|
19
|
+
* (MIT) — base URL resolution, the header set, the `plan_type` /
|
|
20
|
+
* `resets_at` rate-limit wording, and the SSE frame split are reused as
|
|
21
|
+
* DESIGN; no block is copied, and the code here is kiso's.
|
|
22
|
+
*
|
|
23
|
+
* Invariants (the same the two older adapters prove):
|
|
24
|
+
* - `usage` always precedes the final `stop`;
|
|
25
|
+
* - no terminal event ⇒ `usage { known:false }` then `stop { reason:"error" }`;
|
|
26
|
+
* - a tool call's id is captured once and never changes mid-stream;
|
|
27
|
+
* - exactly ONE fetch per stream — no retry loop lives here;
|
|
28
|
+
* - every non-2xx becomes `mapApiError(status, message, retryAfterMs)`.
|
|
29
|
+
*/
|
|
30
|
+
import type { Adapter } from "@vincemakes/kiso-core";
|
|
31
|
+
/** The ChatGPT backend's own token, resolved fresh for each request. */
|
|
32
|
+
export interface ResponsesOAuthToken {
|
|
33
|
+
readonly access: string;
|
|
34
|
+
readonly accountId: string;
|
|
35
|
+
}
|
|
36
|
+
export interface OpenAIResponsesProviderConfig {
|
|
37
|
+
/** First-party target: the API key. */
|
|
38
|
+
readonly apiKey?: string;
|
|
39
|
+
/** ChatGPT target: the token thunk, awaited once per request. */
|
|
40
|
+
readonly oauth?: () => Promise<ResponsesOAuthToken>;
|
|
41
|
+
/** Overrides the target's default base URL (tests and proxies). */
|
|
42
|
+
readonly baseUrl?: string;
|
|
43
|
+
/** The ChatGPT backend's prefix-cache key — the session id, so one
|
|
44
|
+
* session's requests share a cache lane. Sent only to that target. */
|
|
45
|
+
readonly promptCacheKey?: string;
|
|
46
|
+
/** MG-1 (A5): the adapter's replay identity. The kernel STAMPS a
|
|
47
|
+
* turn's continuation with the run's own scope, so the identity this
|
|
48
|
+
* adapter matches against on the NEXT turn must be the one the
|
|
49
|
+
* runtime resolved — passed in rather than re-derived here, so the
|
|
50
|
+
* two sides cannot drift. (They can: an API key configured against
|
|
51
|
+
* the ChatGPT origin resolves to "chatgpt" by origin and "openai" by
|
|
52
|
+
* target, and the symptom would be reasoning that silently never
|
|
53
|
+
* replays.) Absent — a direct SDK consumer — falls back to the
|
|
54
|
+
* target's own identity. */
|
|
55
|
+
readonly scope?: OpenAIResponsesScope;
|
|
56
|
+
}
|
|
57
|
+
/** The provider-identity half of the continuation scope; `apiId` is
|
|
58
|
+
* constant for this adapter and `modelId` is per-request. */
|
|
59
|
+
export interface OpenAIResponsesScope {
|
|
60
|
+
readonly providerId: string;
|
|
61
|
+
}
|
|
62
|
+
export declare function createOpenAIResponsesProvider(config?: OpenAIResponsesProviderConfig): Adapter;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The OpenAI Responses adapter — one adapter, two targets.
|
|
3
|
+
*
|
|
4
|
+
* Plain `fetch` plus a hand-rolled SSE reader: no SDK, no new dependency.
|
|
5
|
+
* The Responses dialect is small enough that a vendor client would buy
|
|
6
|
+
* nothing here and would bring its own retry policy, which this tree
|
|
7
|
+
* refuses (the kernel is the sole retry authority — CX-1 F8).
|
|
8
|
+
*
|
|
9
|
+
* The TARGET is inferred from the options the factory is handed, never
|
|
10
|
+
* from a separate switch that could disagree with them:
|
|
11
|
+
* { apiKey } → the first-party Responses API (api.openai.com/v1/responses)
|
|
12
|
+
* { oauth } → the ChatGPT subscription backend
|
|
13
|
+
* (chatgpt.com/backend-api/codex/responses)
|
|
14
|
+
* The `oauth` thunk is called ONCE PER REQUEST, so a token that expires
|
|
15
|
+
* mid-session is refreshed by whoever owns the credential store — the
|
|
16
|
+
* adapter never caches a token and never refreshes one itself.
|
|
17
|
+
*
|
|
18
|
+
* Design source: the reference implementation's client for this backend
|
|
19
|
+
* (MIT) — base URL resolution, the header set, the `plan_type` /
|
|
20
|
+
* `resets_at` rate-limit wording, and the SSE frame split are reused as
|
|
21
|
+
* DESIGN; no block is copied, and the code here is kiso's.
|
|
22
|
+
*
|
|
23
|
+
* Invariants (the same the two older adapters prove):
|
|
24
|
+
* - `usage` always precedes the final `stop`;
|
|
25
|
+
* - no terminal event ⇒ `usage { known:false }` then `stop { reason:"error" }`;
|
|
26
|
+
* - a tool call's id is captured once and never changes mid-stream;
|
|
27
|
+
* - exactly ONE fetch per stream — no retry loop lives here;
|
|
28
|
+
* - every non-2xx becomes `mapApiError(status, message, retryAfterMs)`.
|
|
29
|
+
*/
|
|
30
|
+
import { mapApiError, parseRetryAfter } from "@vincemakes/kiso-core";
|
|
31
|
+
const FIRST_PARTY_BASE = "https://api.openai.com/v1";
|
|
32
|
+
const CHATGPT_BASE = "https://chatgpt.com/backend-api";
|
|
33
|
+
/**
|
|
34
|
+
* The provider rejects `max_output_tokens` below 16 with a 400. kiso does
|
|
35
|
+
* NOT clamp it: a silently raised cap is a request the caller did not
|
|
36
|
+
* make, and the caller asked for a bound. The refusal happens before the
|
|
37
|
+
* request so the failure names the setting rather than the vendor's 400.
|
|
38
|
+
*/
|
|
39
|
+
const MIN_OUTPUT_TOKENS = 16;
|
|
40
|
+
/** MG-1 (A5): this adapter's continuation entries — one whole reasoning
|
|
41
|
+
* output item, serialized verbatim. */
|
|
42
|
+
const ENTRY_KIND = "openai-responses.item";
|
|
43
|
+
const API_ID = "openai-responses";
|
|
44
|
+
/** `<base>/responses`, tolerating a base that already names the path. */
|
|
45
|
+
function firstPartyUrl(baseUrl) {
|
|
46
|
+
const normalized = baseUrl.replace(/\/+$/, "");
|
|
47
|
+
return normalized.endsWith("/responses") ? normalized : `${normalized}/responses`;
|
|
48
|
+
}
|
|
49
|
+
/** `<base>/codex/responses` — the vendor's own path for this backend,
|
|
50
|
+
* tolerating a base that already names part of it (the reference
|
|
51
|
+
* implementation's resolution, kept as design). */
|
|
52
|
+
function chatgptUrl(baseUrl) {
|
|
53
|
+
const normalized = baseUrl.replace(/\/+$/, "");
|
|
54
|
+
if (normalized.endsWith("/codex/responses"))
|
|
55
|
+
return normalized;
|
|
56
|
+
if (normalized.endsWith("/codex"))
|
|
57
|
+
return `${normalized}/responses`;
|
|
58
|
+
return `${normalized}/codex/responses`;
|
|
59
|
+
}
|
|
60
|
+
function resolveTarget(config) {
|
|
61
|
+
const oauth = config.oauth;
|
|
62
|
+
if (oauth !== undefined) {
|
|
63
|
+
return {
|
|
64
|
+
providerId: "chatgpt",
|
|
65
|
+
url: chatgptUrl(config.baseUrl ?? CHATGPT_BASE),
|
|
66
|
+
headers: async () => {
|
|
67
|
+
const token = await oauth();
|
|
68
|
+
return {
|
|
69
|
+
authorization: `Bearer ${token.access}`,
|
|
70
|
+
"chatgpt-account-id": token.accountId,
|
|
71
|
+
originator: "kiso",
|
|
72
|
+
"OpenAI-Beta": "responses=experimental",
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
// The backend rejects `store: true`; with nothing stored, the
|
|
76
|
+
// reasoning items have to come back on the next turn, which is
|
|
77
|
+
// what `include` asks for and what the continuation replays.
|
|
78
|
+
extraBody: {
|
|
79
|
+
store: false,
|
|
80
|
+
include: ["reasoning.encrypted_content"],
|
|
81
|
+
...(config.promptCacheKey !== undefined ? { prompt_cache_key: config.promptCacheKey } : {}),
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
providerId: "openai",
|
|
87
|
+
url: firstPartyUrl(config.baseUrl ?? FIRST_PARTY_BASE),
|
|
88
|
+
headers: async () => ({ authorization: `Bearer ${config.apiKey ?? ""}` }),
|
|
89
|
+
extraBody: {},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export function createOpenAIResponsesProvider(config = {}) {
|
|
93
|
+
const target = resolveTarget(config);
|
|
94
|
+
const scopeProviderId = config.scope?.providerId ?? target.providerId;
|
|
95
|
+
return {
|
|
96
|
+
async *stream(options) {
|
|
97
|
+
if (options.maxTokens !== undefined && options.maxTokens < MIN_OUTPUT_TOKENS) {
|
|
98
|
+
throw {
|
|
99
|
+
code: "invalid_request",
|
|
100
|
+
retryable: false,
|
|
101
|
+
message: `[${target.providerId}] maxTokens ${options.maxTokens} is below the Responses API floor of ${MIN_OUTPUT_TOKENS} — raise it; kiso never silently raises a cap you set`,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const body = buildBody(options, target, scopeProviderId);
|
|
105
|
+
const response = await requestOnce(target, body, options.signal);
|
|
106
|
+
yield* mapStream(response, options, target, scopeProviderId);
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
// ── The request ────────────────────────────────────────────────────────
|
|
111
|
+
function buildBody(options, target, scopeProviderId) {
|
|
112
|
+
return {
|
|
113
|
+
model: options.model,
|
|
114
|
+
stream: true,
|
|
115
|
+
// The system prompt is the Responses dialect's `instructions`.
|
|
116
|
+
// Absent means ABSENT — no default assistant prompt is invented.
|
|
117
|
+
...(options.systemPrompt !== undefined ? { instructions: options.systemPrompt } : {}),
|
|
118
|
+
input: toInput(options.messages, options.model, scopeProviderId),
|
|
119
|
+
...(options.tools?.length ? { tools: options.tools.map(toResponsesTool) } : {}),
|
|
120
|
+
// XP-1: the RESOLVED effort. The registry's `wire` string names the
|
|
121
|
+
// dialect parameter for a human reader and never reaches an
|
|
122
|
+
// adapter — the dialect path is this adapter's own knowledge, and
|
|
123
|
+
// the level is transported as handed. Absent adds NO key.
|
|
124
|
+
...(options.reasoning?.effort !== undefined ? { reasoning: { effort: options.reasoning.effort } } : {}),
|
|
125
|
+
...(options.maxTokens !== undefined ? { max_output_tokens: options.maxTokens } : {}),
|
|
126
|
+
...(options.temperature !== undefined ? { temperature: options.temperature } : {}),
|
|
127
|
+
...target.extraBody,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function toResponsesTool(tool) {
|
|
131
|
+
// No `strict`: the flag changes the PROVIDER's validation semantics,
|
|
132
|
+
// and kiso's schemas are already closed worlds validated by the kernel
|
|
133
|
+
// (PH-1a.1). Sending it would claim a second, disagreeing validator.
|
|
134
|
+
return { type: "function", name: tool.name, description: tool.description, parameters: tool.inputSchema };
|
|
135
|
+
}
|
|
136
|
+
function toInputContent(content) {
|
|
137
|
+
if (typeof content === "string")
|
|
138
|
+
return [{ type: "input_text", text: content }];
|
|
139
|
+
return content.map((block) => block.type === "text"
|
|
140
|
+
? { type: "input_text", text: block.text }
|
|
141
|
+
: {
|
|
142
|
+
type: "input_image",
|
|
143
|
+
detail: "auto",
|
|
144
|
+
// A base64 block becomes a REAL data URL; a URL-sourced
|
|
145
|
+
// block passes the provider URL through.
|
|
146
|
+
image_url: block.sourceType === "base64"
|
|
147
|
+
? `data:${block.mediaType ?? "image/png"};base64,${block.data ?? ""}`
|
|
148
|
+
: (block.url ?? ""),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/** A tool result is text on this dialect. An image is turned into an
|
|
152
|
+
* EXPLICIT note saying what was omitted and why — never dropped in
|
|
153
|
+
* silence (the same honesty the compat adapter keeps). */
|
|
154
|
+
function toToolResultOutput(content) {
|
|
155
|
+
if (typeof content === "string")
|
|
156
|
+
return content;
|
|
157
|
+
return content
|
|
158
|
+
.map((b) => b.type === "text"
|
|
159
|
+
? b.text
|
|
160
|
+
: `[image omitted — Responses tool results carry text only: ${b.sourceType === "base64" ? `${b.mediaType ?? "image"} (${b.data?.length ?? 0} base64 chars)` : `url ${b.url ?? ""}`}]`)
|
|
161
|
+
.join("");
|
|
162
|
+
}
|
|
163
|
+
function toOutputItem(block) {
|
|
164
|
+
if (block.type === "text") {
|
|
165
|
+
// No `id`: this dialect accepts a replayed assistant message
|
|
166
|
+
// without one, and inventing an item id would be a fabricated
|
|
167
|
+
// provider fact.
|
|
168
|
+
return {
|
|
169
|
+
type: "message",
|
|
170
|
+
role: "assistant",
|
|
171
|
+
content: [{ type: "output_text", text: block.text, annotations: [] }],
|
|
172
|
+
status: "completed",
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
// No `id` here either — an item id pairs a function_call with a stored
|
|
176
|
+
// reasoning item, and under `store: false` there is nothing to pair
|
|
177
|
+
// with. `call_id` is the identity the kernel and the provider share.
|
|
178
|
+
return { type: "function_call", call_id: block.callId, name: block.name, arguments: JSON.stringify(block.input) };
|
|
179
|
+
}
|
|
180
|
+
/** MG-1 (A5): the stored reasoning items replay ONLY to the scope that
|
|
181
|
+
* produced them — provider AND api AND model. A mismatched or absent
|
|
182
|
+
* envelope replays nothing; an entry that no longer parses is skipped
|
|
183
|
+
* rather than killing the request. */
|
|
184
|
+
function continuationItems(msg, model, providerId) {
|
|
185
|
+
const c = msg.continuation;
|
|
186
|
+
if (c === undefined)
|
|
187
|
+
return [];
|
|
188
|
+
const s = c.scope;
|
|
189
|
+
if (s.providerId !== providerId || s.apiId !== API_ID || s.modelId !== model)
|
|
190
|
+
return [];
|
|
191
|
+
const out = [];
|
|
192
|
+
for (const e of c.entries) {
|
|
193
|
+
if (e.kind !== ENTRY_KIND)
|
|
194
|
+
continue;
|
|
195
|
+
try {
|
|
196
|
+
out.push(JSON.parse(e.data));
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
// opaque bytes that no longer parse must not kill the request
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return out;
|
|
203
|
+
}
|
|
204
|
+
function toInput(messages, model, providerId) {
|
|
205
|
+
const out = [];
|
|
206
|
+
for (const msg of messages) {
|
|
207
|
+
if (msg.role === "user") {
|
|
208
|
+
out.push({ role: "user", content: toInputContent(msg.content) });
|
|
209
|
+
}
|
|
210
|
+
else if (msg.role === "assistant") {
|
|
211
|
+
// The reasoning items come FIRST, in emission order: the
|
|
212
|
+
// provider reads the turn back in the order it produced it.
|
|
213
|
+
out.push(...continuationItems(msg, model, providerId), ...msg.blocks.map(toOutputItem));
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
out.push({ type: "function_call_output", call_id: msg.callId, output: toToolResultOutput(msg.content) });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return out;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* EXACTLY ONE fetch. No retry loop lives here: the kernel owns the retry
|
|
223
|
+
* budget, and a second attempt underneath it would run outside that
|
|
224
|
+
* budget and outside the request trace (CX-1 F8).
|
|
225
|
+
*/
|
|
226
|
+
async function requestOnce(target, body, signal) {
|
|
227
|
+
const headers = {
|
|
228
|
+
"content-type": "application/json",
|
|
229
|
+
accept: "text/event-stream",
|
|
230
|
+
...(await target.headers()),
|
|
231
|
+
};
|
|
232
|
+
let response;
|
|
233
|
+
try {
|
|
234
|
+
response = await fetch(target.url, {
|
|
235
|
+
method: "POST",
|
|
236
|
+
headers,
|
|
237
|
+
body: JSON.stringify(body),
|
|
238
|
+
...(signal !== undefined ? { signal: signal } : {}),
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
catch (err) {
|
|
242
|
+
throw toTransportError(err, target.providerId);
|
|
243
|
+
}
|
|
244
|
+
if (!response.ok)
|
|
245
|
+
throw await toHttpError(response, target.providerId);
|
|
246
|
+
return response;
|
|
247
|
+
}
|
|
248
|
+
async function* mapStream(response, options, target, scopeProviderId) {
|
|
249
|
+
const state = { slots: new Map(), entries: [], sawToolCall: false, terminal: false };
|
|
250
|
+
const frames = readSSE(response, options.signal, target.providerId);
|
|
251
|
+
try {
|
|
252
|
+
for (;;) {
|
|
253
|
+
let step;
|
|
254
|
+
try {
|
|
255
|
+
step = await frames.next();
|
|
256
|
+
}
|
|
257
|
+
catch (err) {
|
|
258
|
+
// A cancellation is the CALLER's act and a protocol failure
|
|
259
|
+
// is the provider's — both propagate unchanged. What is left
|
|
260
|
+
// is the connection dying mid-stream: a RETRYABLE network
|
|
261
|
+
// error, thrown as the two older adapters throw it, so the
|
|
262
|
+
// kernel's stream-cut recovery (F4: void the draft durably,
|
|
263
|
+
// then retry) engages. The trailing guard below is for a
|
|
264
|
+
// stream that ENDED cleanly without a terminal frame — a
|
|
265
|
+
// truncated turn the provider chose to end, not a cut.
|
|
266
|
+
if (isAbort(err) || options.signal?.aborted || isStructured(err))
|
|
267
|
+
throw err;
|
|
268
|
+
throw toTransportError(err, target.providerId);
|
|
269
|
+
}
|
|
270
|
+
if (step.done)
|
|
271
|
+
break;
|
|
272
|
+
// Outside the try on purpose: a mapping failure (unparseable
|
|
273
|
+
// tool arguments, an id that changed, an error frame) is this
|
|
274
|
+
// adapter's own verdict and must never be mistaken for a cut.
|
|
275
|
+
yield* mapFrame(step.value, state, options.model, target.providerId, scopeProviderId);
|
|
276
|
+
if (state.terminal)
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
finally {
|
|
281
|
+
// Closes the reader and removes the abort listener on EVERY exit —
|
|
282
|
+
// including the consumer abandoning the iteration mid-turn.
|
|
283
|
+
await frames.return(undefined);
|
|
284
|
+
}
|
|
285
|
+
if (state.terminal)
|
|
286
|
+
return;
|
|
287
|
+
// The trailing guard: a stream that never reached a terminal response
|
|
288
|
+
// is a truncated turn. Usage is UNKNOWN (nulls, never a free turn),
|
|
289
|
+
// the stop is an explicit error, and nothing follows it.
|
|
290
|
+
yield usageEvent(undefined);
|
|
291
|
+
yield stopEvent("error", state, options.model, scopeProviderId);
|
|
292
|
+
}
|
|
293
|
+
function* mapFrame(frame, state, model, providerId, scopeProviderId) {
|
|
294
|
+
const index = frame.output_index ?? 0;
|
|
295
|
+
switch (frame.type) {
|
|
296
|
+
case "response.output_item.added": {
|
|
297
|
+
const item = frame.item;
|
|
298
|
+
if (item === undefined)
|
|
299
|
+
break;
|
|
300
|
+
if (item.type === "message") {
|
|
301
|
+
state.slots.set(index, { kind: "text" });
|
|
302
|
+
yield { seq: 0, type: "text_start" };
|
|
303
|
+
}
|
|
304
|
+
else if (item.type === "function_call") {
|
|
305
|
+
yield* openToolCall(state, index, item);
|
|
306
|
+
}
|
|
307
|
+
// A reasoning item needs no slot: its text arrives as flat
|
|
308
|
+
// `thinking` events that carry no index, and the item itself
|
|
309
|
+
// is captured whole at its done frame.
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
case "response.output_text.delta":
|
|
313
|
+
case "response.refusal.delta": {
|
|
314
|
+
if (frame.delta === undefined)
|
|
315
|
+
break;
|
|
316
|
+
// A delta never precedes its start: an index that has no open
|
|
317
|
+
// text block gets one here rather than the text being dropped.
|
|
318
|
+
if (state.slots.get(index)?.kind !== "text") {
|
|
319
|
+
state.slots.set(index, { kind: "text" });
|
|
320
|
+
yield { seq: 0, type: "text_start" };
|
|
321
|
+
}
|
|
322
|
+
yield { seq: 0, type: "text_delta", text: frame.delta };
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
case "response.reasoning_text.delta":
|
|
326
|
+
case "response.reasoning_summary_text.delta":
|
|
327
|
+
// ONE flat `thinking` event — the contract has no
|
|
328
|
+
// start/delta/end for reasoning, and the two reasoning
|
|
329
|
+
// dialects (raw text and summary) are the same stream to a
|
|
330
|
+
// reader.
|
|
331
|
+
if (frame.delta !== undefined && frame.delta !== "")
|
|
332
|
+
yield { seq: 0, type: "thinking", text: frame.delta };
|
|
333
|
+
break;
|
|
334
|
+
case "response.function_call_arguments.delta": {
|
|
335
|
+
const slot = state.slots.get(index);
|
|
336
|
+
// An arguments delta carries no identity of its own, so an
|
|
337
|
+
// index with no open call has nothing to attribute it to.
|
|
338
|
+
if (slot?.kind !== "tool" || frame.delta === undefined)
|
|
339
|
+
break;
|
|
340
|
+
slot.sent += frame.delta;
|
|
341
|
+
yield { seq: 0, type: "tool_call_input_delta", callId: slot.callId, inputJsonDelta: frame.delta };
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
case "response.function_call_arguments.done": {
|
|
345
|
+
const slot = state.slots.get(index);
|
|
346
|
+
const args = frame.arguments;
|
|
347
|
+
if (slot?.kind !== "tool" || args === undefined)
|
|
348
|
+
break;
|
|
349
|
+
// Only the SUFFIX the deltas did not carry: the accumulated
|
|
350
|
+
// input must equal the arguments exactly once. A final value
|
|
351
|
+
// that is not an extension of what was streamed emits nothing
|
|
352
|
+
// — the authoritative arguments still arrive with the item's
|
|
353
|
+
// done frame.
|
|
354
|
+
if (!args.startsWith(slot.sent))
|
|
355
|
+
break;
|
|
356
|
+
const suffix = args.slice(slot.sent.length);
|
|
357
|
+
slot.sent = args;
|
|
358
|
+
if (suffix !== "")
|
|
359
|
+
yield { seq: 0, type: "tool_call_input_delta", callId: slot.callId, inputJsonDelta: suffix };
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
case "response.output_item.done": {
|
|
363
|
+
const item = frame.item;
|
|
364
|
+
if (item === undefined)
|
|
365
|
+
break;
|
|
366
|
+
if (item.type === "message") {
|
|
367
|
+
state.slots.delete(index);
|
|
368
|
+
yield { seq: 0, type: "text_end" };
|
|
369
|
+
}
|
|
370
|
+
else if (item.type === "function_call") {
|
|
371
|
+
yield* closeToolCall(state, index, item, providerId);
|
|
372
|
+
}
|
|
373
|
+
else if (item.type === "reasoning") {
|
|
374
|
+
// MG-1 (A5): the WHOLE item, verbatim — the encrypted
|
|
375
|
+
// payload IS the model's state, and only an item that
|
|
376
|
+
// carries one is worth replaying (the first-party target
|
|
377
|
+
// asks for no encrypted content, so it produces none).
|
|
378
|
+
if (typeof item.encrypted_content === "string" && item.encrypted_content !== "") {
|
|
379
|
+
state.entries.push({ kind: ENTRY_KIND, required: true, data: JSON.stringify(item) });
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
case "response.completed":
|
|
385
|
+
case "response.incomplete": {
|
|
386
|
+
state.terminal = true;
|
|
387
|
+
const response = frame.response;
|
|
388
|
+
// usage BEFORE stop, both read from the SAME frame: a usage
|
|
389
|
+
// taken from anywhere else would belong to another response.
|
|
390
|
+
yield usageEvent(response?.usage ?? undefined);
|
|
391
|
+
yield stopEvent(stopReasonOf(response, state.sawToolCall), state, model, scopeProviderId);
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
case "error":
|
|
395
|
+
throw mapApiError(undefined, `[${providerId}] stream error: ${frame.message ?? frame.code ?? "no detail"}`);
|
|
396
|
+
case "response.failed":
|
|
397
|
+
throw mapApiError(undefined, `[${providerId}] response failed: ${failureDetail(frame.response)}`);
|
|
398
|
+
default:
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
function* openToolCall(state, index, item) {
|
|
403
|
+
const callId = item.call_id ?? "";
|
|
404
|
+
const name = item.name ?? "";
|
|
405
|
+
state.slots.set(index, { kind: "tool", callId, name, sent: item.arguments ?? "" });
|
|
406
|
+
yield { seq: 0, type: "tool_call_start", callId, name };
|
|
407
|
+
}
|
|
408
|
+
function* closeToolCall(state, index, item, providerId) {
|
|
409
|
+
const open = state.slots.get(index);
|
|
410
|
+
const callId = item.call_id ?? "";
|
|
411
|
+
if (open?.kind !== "tool") {
|
|
412
|
+
// The done frame carries the whole identity, so a call whose added
|
|
413
|
+
// frame never arrived still gets its start before its end — an end
|
|
414
|
+
// without a start would be a forged event order.
|
|
415
|
+
yield* openToolCall(state, index, item);
|
|
416
|
+
}
|
|
417
|
+
else if (open.callId !== callId) {
|
|
418
|
+
// round 9's rule, one dialect over: the id is captured ONCE. A
|
|
419
|
+
// different id under the same index is a protocol violation, never
|
|
420
|
+
// a silent switch — start, deltas and end share one identity.
|
|
421
|
+
throw {
|
|
422
|
+
code: "invalid_request",
|
|
423
|
+
retryable: false,
|
|
424
|
+
message: `[${providerId}] tool call at output index ${index} changed id mid-stream: ${open.callId} → ${callId}`,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
state.slots.delete(index);
|
|
428
|
+
state.sawToolCall = true;
|
|
429
|
+
const raw = item.arguments === undefined || item.arguments === "" ? "{}" : item.arguments;
|
|
430
|
+
let parsed;
|
|
431
|
+
try {
|
|
432
|
+
parsed = JSON.parse(raw);
|
|
433
|
+
}
|
|
434
|
+
catch {
|
|
435
|
+
parsed = undefined;
|
|
436
|
+
}
|
|
437
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
438
|
+
// NEVER a silent repair and never a null input passed off as an
|
|
439
|
+
// empty call: the model asked for something this adapter cannot
|
|
440
|
+
// state, so the turn fails loudly.
|
|
441
|
+
throw {
|
|
442
|
+
code: "invalid_request",
|
|
443
|
+
retryable: false,
|
|
444
|
+
message: `[${providerId}] tool call ${callId} sent arguments that are not a JSON object: ${raw.slice(0, 200)}`,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
yield {
|
|
448
|
+
seq: 0,
|
|
449
|
+
type: "tool_call_end",
|
|
450
|
+
callId,
|
|
451
|
+
name: item.name ?? (open?.kind === "tool" ? open.name : ""),
|
|
452
|
+
input: parsed,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
function usageEvent(usage) {
|
|
456
|
+
if (usage === undefined || usage === null) {
|
|
457
|
+
return { seq: 0, type: "usage", inputTokens: null, outputTokens: null, cacheRead: null, cacheWrite: null, known: false };
|
|
458
|
+
}
|
|
459
|
+
return {
|
|
460
|
+
seq: 0,
|
|
461
|
+
type: "usage",
|
|
462
|
+
// RAW, as the provider reports it: `input_tokens` INCLUDES the
|
|
463
|
+
// cached prefix on this dialect, and the runtime's canonicalizer
|
|
464
|
+
// owns the subtraction. Doing it here too would bill the cached
|
|
465
|
+
// tokens away twice.
|
|
466
|
+
inputTokens: usage.input_tokens ?? null,
|
|
467
|
+
outputTokens: usage.output_tokens ?? null,
|
|
468
|
+
cacheRead: usage.input_tokens_details?.cached_tokens ?? null,
|
|
469
|
+
// The Responses API reports no cache-creation count; null is the
|
|
470
|
+
// honest answer, never a zero.
|
|
471
|
+
cacheWrite: null,
|
|
472
|
+
known: true,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
function stopEvent(reason, state, model, scopeProviderId) {
|
|
476
|
+
return {
|
|
477
|
+
seq: 0,
|
|
478
|
+
type: "stop",
|
|
479
|
+
reason,
|
|
480
|
+
// A5: the captured items ride the stop as opaque entries with a
|
|
481
|
+
// self-reported scope; the kernel re-stamps it (adapters are not
|
|
482
|
+
// trusted). `required` is unconditional here: under `store: false`
|
|
483
|
+
// the next request is INVALID without them.
|
|
484
|
+
...(state.entries.length > 0
|
|
485
|
+
? { continuation: { scope: { providerId: scopeProviderId, apiId: API_ID, modelId: model }, entries: state.entries } }
|
|
486
|
+
: {}),
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
function stopReasonOf(response, sawToolCall) {
|
|
490
|
+
if (response?.status === "completed")
|
|
491
|
+
return sawToolCall ? "tool_use" : "end_turn";
|
|
492
|
+
if (response?.status === "incomplete") {
|
|
493
|
+
return response.incomplete_details?.reason === "max_output_tokens" ? "max_tokens" : "error";
|
|
494
|
+
}
|
|
495
|
+
// failed, cancelled, a status this adapter has never seen, or none at
|
|
496
|
+
// all: an error, never degraded into a clean end.
|
|
497
|
+
return "error";
|
|
498
|
+
}
|
|
499
|
+
function failureDetail(response) {
|
|
500
|
+
const error = response?.error;
|
|
501
|
+
if (error !== undefined && error !== null)
|
|
502
|
+
return `${error.code ?? "unknown"}: ${error.message ?? "no message"}`;
|
|
503
|
+
const reason = response?.incomplete_details?.reason;
|
|
504
|
+
return reason !== undefined ? `incomplete: ${reason}` : "no error details in the response";
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* The SSE reader: frames are separated by a blank line, and a frame's
|
|
508
|
+
* `data:` lines join with newlines. `[DONE]` is a sentinel, not an event.
|
|
509
|
+
* The abort signal reaches BOTH the fetch (above) and this reader, and
|
|
510
|
+
* the listener is removed in `finally` — an adapter that leaves a
|
|
511
|
+
* listener on a long-lived signal leaks one per turn.
|
|
512
|
+
*/
|
|
513
|
+
async function* readSSE(response, signal, providerId) {
|
|
514
|
+
const body = response.body;
|
|
515
|
+
if (body === null)
|
|
516
|
+
return;
|
|
517
|
+
const reader = body.getReader();
|
|
518
|
+
const decoder = new TextDecoder();
|
|
519
|
+
const onAbort = () => {
|
|
520
|
+
void reader.cancel().catch(() => { });
|
|
521
|
+
};
|
|
522
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
523
|
+
let buffer = "";
|
|
524
|
+
try {
|
|
525
|
+
for (;;) {
|
|
526
|
+
const { done, value } = await reader.read();
|
|
527
|
+
// Checked after the read, not only before: a cancel resolves a
|
|
528
|
+
// pending read rather than rejecting it, and a turn the caller
|
|
529
|
+
// stopped must not look like a clean end of stream.
|
|
530
|
+
if (signal?.aborted)
|
|
531
|
+
throw abortError();
|
|
532
|
+
if (done)
|
|
533
|
+
break;
|
|
534
|
+
buffer += decoder.decode(value, { stream: true });
|
|
535
|
+
let idx = buffer.indexOf("\n\n");
|
|
536
|
+
while (idx !== -1) {
|
|
537
|
+
const frame = buffer.slice(0, idx);
|
|
538
|
+
buffer = buffer.slice(idx + 2);
|
|
539
|
+
const data = frame
|
|
540
|
+
.split("\n")
|
|
541
|
+
.filter((l) => l.startsWith("data:"))
|
|
542
|
+
.map((l) => l.slice(5).trim())
|
|
543
|
+
.join("\n");
|
|
544
|
+
if (data !== "" && data !== "[DONE]")
|
|
545
|
+
yield parseFrame(data, providerId);
|
|
546
|
+
idx = buffer.indexOf("\n\n");
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
finally {
|
|
551
|
+
signal?.removeEventListener("abort", onAbort);
|
|
552
|
+
try {
|
|
553
|
+
await reader.cancel();
|
|
554
|
+
}
|
|
555
|
+
catch {
|
|
556
|
+
// already closed or errored — there is nothing left to release
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
function parseFrame(data, providerId) {
|
|
561
|
+
try {
|
|
562
|
+
return JSON.parse(data);
|
|
563
|
+
}
|
|
564
|
+
catch {
|
|
565
|
+
// A frame that is not JSON is a PROTOCOL failure, not a truncated
|
|
566
|
+
// turn: it is reported rather than folded into the trailing guard.
|
|
567
|
+
throw mapApiError(undefined, `[${providerId}] malformed SSE frame: ${data.slice(0, 200)}`);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
function abortError() {
|
|
571
|
+
const err = new Error("the request was aborted");
|
|
572
|
+
err.name = "AbortError";
|
|
573
|
+
return err;
|
|
574
|
+
}
|
|
575
|
+
function isAbort(err) {
|
|
576
|
+
return err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError");
|
|
577
|
+
}
|
|
578
|
+
/** Already a StructuredError (this adapter's own verdict, or the kernel's
|
|
579
|
+
* shape from mapApiError) — it travels unchanged. */
|
|
580
|
+
function isStructured(err) {
|
|
581
|
+
return typeof err === "object" && err !== null && "code" in err && "retryable" in err;
|
|
582
|
+
}
|
|
583
|
+
// ── Errors ─────────────────────────────────────────────────────────────
|
|
584
|
+
/** `retry-after-ms` (the vendor's millisecond header) wins over the HTTP
|
|
585
|
+
* `Retry-After`; neither is ever shortened. */
|
|
586
|
+
function retryAfterOf(headers) {
|
|
587
|
+
const ms = headers.get("retry-after-ms");
|
|
588
|
+
if (ms !== null && /^\d+$/.test(ms.trim()))
|
|
589
|
+
return Number(ms.trim());
|
|
590
|
+
return parseRetryAfter(headers.get("retry-after"));
|
|
591
|
+
}
|
|
592
|
+
async function toHttpError(response, providerId) {
|
|
593
|
+
const raw = await response.text().catch(() => "");
|
|
594
|
+
let message = raw !== "" ? raw : response.statusText || "request failed";
|
|
595
|
+
let planNote = "";
|
|
596
|
+
try {
|
|
597
|
+
const parsed = JSON.parse(raw);
|
|
598
|
+
const err = parsed.error;
|
|
599
|
+
if (err !== undefined && err !== null) {
|
|
600
|
+
if (typeof err.message === "string" && err.message !== "")
|
|
601
|
+
message = err.message;
|
|
602
|
+
// The subscription backend's quota facts belong in the MESSAGE a
|
|
603
|
+
// human reads. They never become retryAfterMs: `resets_at` is
|
|
604
|
+
// when the plan's window rolls over, not when this request may
|
|
605
|
+
// be retried, and feeding it to the kernel's backoff would park
|
|
606
|
+
// a session for hours on the provider's say-so. The label
|
|
607
|
+
// already names WHICH provider said it, so the note does not.
|
|
608
|
+
const plan = typeof err.plan_type === "string" ? ` (${err.plan_type.toLowerCase()} plan)` : "";
|
|
609
|
+
const mins = typeof err.resets_at === "number" ? Math.max(0, Math.round((err.resets_at * 1000 - Date.now()) / 60000)) : undefined;
|
|
610
|
+
if (plan !== "" || mins !== undefined) {
|
|
611
|
+
planNote = ` — usage limit${plan}${mins !== undefined ? `, resets in ~${mins} min` : ""}`;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
catch {
|
|
616
|
+
// not JSON: the raw body is the message, which is the honest one
|
|
617
|
+
}
|
|
618
|
+
const signIn = response.status === 401 && providerId === "chatgpt" ? " — run `kiso login chatgpt`" : "";
|
|
619
|
+
return mapApiError(response.status, `[${providerId}] request failed: ${message}${planNote}${signIn}`, retryAfterOf(response.headers));
|
|
620
|
+
}
|
|
621
|
+
function toTransportError(err, providerId) {
|
|
622
|
+
// A cancellation is the CALLER's own act: it propagates unchanged, so
|
|
623
|
+
// it never reaches the kernel wearing `retryable: true` and gets the
|
|
624
|
+
// turn the caller stopped run again.
|
|
625
|
+
if (isAbort(err))
|
|
626
|
+
return err;
|
|
627
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
628
|
+
return { code: "network", retryable: true, message: `[${providerId}] request failed: ${message}` };
|
|
629
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vincemakes/kiso-provider-openai-responses",
|
|
3
|
+
"version": "0.31.0",
|
|
4
|
+
"description": "kiso OpenAI Responses adapter — the first-party Responses API and the ChatGPT subscription backend, one adapter, plain fetch + SSE.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc -p tsconfig.build.json",
|
|
20
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
21
|
+
"test": "vitest run"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@vincemakes/kiso-core": "0.31.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^26.1.2",
|
|
28
|
+
"typescript": "^5.7.2",
|
|
29
|
+
"vitest": "^3.0.0"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=22"
|
|
33
|
+
},
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/vincemakes/kiso.git",
|
|
37
|
+
"directory": "packages/provider-openai-responses"
|
|
38
|
+
},
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/vincemakes/kiso/issues"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://github.com/vincemakes/kiso/tree/main/packages/provider-openai-responses#readme"
|
|
43
|
+
}
|