@visual-z/xeno 0.1.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/README.md +498 -0
- package/docs/install.md +130 -0
- package/package.json +43 -0
- package/src/apps/approve.ts +49 -0
- package/src/apps/cli.ts +37 -0
- package/src/apps/commands.ts +226 -0
- package/src/apps/completions.ts +28 -0
- package/src/apps/input.ts +297 -0
- package/src/apps/render.ts +207 -0
- package/src/apps/shell.ts +2353 -0
- package/src/apps/startup.ts +1302 -0
- package/src/core/benchmark.ts +195 -0
- package/src/core/boundary.ts +174 -0
- package/src/core/churn.ts +59 -0
- package/src/core/completion-review.ts +66 -0
- package/src/core/context-policy.ts +60 -0
- package/src/core/context-window.ts +54 -0
- package/src/core/continuation.ts +114 -0
- package/src/core/cycle.ts +17 -0
- package/src/core/data-query.ts +201 -0
- package/src/core/deadline.ts +39 -0
- package/src/core/derive.ts +489 -0
- package/src/core/edit-plan.ts +200 -0
- package/src/core/environment.ts +29 -0
- package/src/core/event-validation.ts +99 -0
- package/src/core/events.ts +486 -0
- package/src/core/evidence-links.ts +71 -0
- package/src/core/evidence.ts +89 -0
- package/src/core/execution.ts +12 -0
- package/src/core/exhaustive.ts +18 -0
- package/src/core/experiment.ts +40 -0
- package/src/core/export.ts +169 -0
- package/src/core/file-references.ts +35 -0
- package/src/core/frontmatter.ts +213 -0
- package/src/core/handoff.ts +397 -0
- package/src/core/harness-comparison.ts +90 -0
- package/src/core/history-query.ts +103 -0
- package/src/core/history.ts +100 -0
- package/src/core/json-event.ts +135 -0
- package/src/core/json-pointer.ts +46 -0
- package/src/core/keymap.ts +241 -0
- package/src/core/observation-view.ts +87 -0
- package/src/core/outcome.ts +45 -0
- package/src/core/overflow.ts +59 -0
- package/src/core/permissions.ts +182 -0
- package/src/core/policy-evaluation.ts +55 -0
- package/src/core/progress.ts +101 -0
- package/src/core/project.ts +372 -0
- package/src/core/prompts.ts +83 -0
- package/src/core/queue.ts +60 -0
- package/src/core/read-bounds.ts +15 -0
- package/src/core/recall-analysis.ts +90 -0
- package/src/core/recall-trial.ts +153 -0
- package/src/core/response-recovery.ts +46 -0
- package/src/core/scheduling.ts +8 -0
- package/src/core/settings.ts +174 -0
- package/src/core/skills.ts +121 -0
- package/src/core/stall.ts +48 -0
- package/src/core/text-normalize.ts +81 -0
- package/src/core/theme.ts +179 -0
- package/src/core/tokens.ts +149 -0
- package/src/core/tool-contract.ts +102 -0
- package/src/core/tool-input.ts +38 -0
- package/src/core/transcript.ts +60 -0
- package/src/core/window-trial.ts +65 -0
- package/src/core/work-state.ts +103 -0
- package/src/core/yaml.ts +282 -0
- package/src/index.ts +10 -0
- package/src/runtime/auth.ts +255 -0
- package/src/runtime/benchmark.ts +187 -0
- package/src/runtime/catalog.ts +365 -0
- package/src/runtime/evidence-notes.ts +33 -0
- package/src/runtime/exec-sessions.ts +214 -0
- package/src/runtime/execution-control.ts +39 -0
- package/src/runtime/harness-comparison.ts +356 -0
- package/src/runtime/history-tools.ts +108 -0
- package/src/runtime/loop.ts +1983 -0
- package/src/runtime/model.ts +614 -0
- package/src/runtime/models-fetch.ts +155 -0
- package/src/runtime/oauth-device.ts +86 -0
- package/src/runtime/oauth.ts +759 -0
- package/src/runtime/openai-responses.ts +286 -0
- package/src/runtime/openai.ts +675 -0
- package/src/runtime/provider.ts +498 -0
- package/src/runtime/recall-trial.ts +226 -0
- package/src/runtime/resume.ts +66 -0
- package/src/runtime/retry.ts +297 -0
- package/src/runtime/tools.ts +1193 -0
- package/src/runtime/usage.ts +225 -0
- package/src/runtime/window-trial.ts +140 -0
- package/src/runtime/working-notes.ts +30 -0
- package/src/store/atomic.ts +31 -0
- package/src/store/auth-store.ts +50 -0
- package/src/store/blobs.ts +142 -0
- package/src/store/command.ts +75 -0
- package/src/store/config.ts +69 -0
- package/src/store/context-files.ts +356 -0
- package/src/store/file-index.ts +81 -0
- package/src/store/git-branch.ts +35 -0
- package/src/store/history-search.ts +19 -0
- package/src/store/log.ts +642 -0
- package/src/store/models.ts +41 -0
- package/src/store/observations.ts +181 -0
- package/src/store/query-source.ts +44 -0
- package/src/store/recovery.ts +39 -0
- package/src/store/replay.ts +63 -0
- package/src/store/resources.ts +361 -0
- package/src/store/restore.ts +302 -0
- package/src/store/search.ts +644 -0
- package/src/store/serial.ts +13 -0
- package/src/store/sessions.ts +463 -0
- package/src/store/trust.ts +69 -0
- package/src/store/workspace.ts +256 -0
- package/src/store/worktree.ts +233 -0
- package/src/tui/ansi.ts +67 -0
- package/src/tui/autocomplete.ts +29 -0
- package/src/tui/complete.ts +87 -0
- package/src/tui/diff.ts +160 -0
- package/src/tui/editor.ts +893 -0
- package/src/tui/exhaustive.ts +14 -0
- package/src/tui/fuzzy.ts +75 -0
- package/src/tui/host.ts +223 -0
- package/src/tui/input.ts +69 -0
- package/src/tui/keys.ts +378 -0
- package/src/tui/markdown.ts +17 -0
- package/src/tui/render.ts +153 -0
- package/src/tui/select.ts +240 -0
- package/src/tui/settings.ts +40 -0
- package/src/tui/statusline.ts +162 -0
- package/src/tui/style.ts +205 -0
- package/src/tui/terminal.ts +30 -0
- package/src/tui/text.ts +52 -0
- package/src/tui/toolview.ts +386 -0
- package/src/tui/transcript.ts +71 -0
- package/src/tui/tree.ts +405 -0
- package/src/tui/width.ts +567 -0
- package/src/vendor/pi-tui/LICENSE.upstream +21 -0
- package/src/vendor/pi-tui/VENDOR.md +23 -0
- package/src/vendor/pi-tui/alt-screen-search.ts +327 -0
- package/src/vendor/pi-tui/autocomplete.ts +835 -0
- package/src/vendor/pi-tui/components/alt-screen-flash.ts +51 -0
- package/src/vendor/pi-tui/components/box.ts +167 -0
- package/src/vendor/pi-tui/components/cancellable-loader.ts +40 -0
- package/src/vendor/pi-tui/components/editor.ts +2470 -0
- package/src/vendor/pi-tui/components/h-stack.ts +44 -0
- package/src/vendor/pi-tui/components/image.ts +127 -0
- package/src/vendor/pi-tui/components/input.ts +494 -0
- package/src/vendor/pi-tui/components/loader.ts +101 -0
- package/src/vendor/pi-tui/components/markdown.ts +1015 -0
- package/src/vendor/pi-tui/components/mouse-region.ts +33 -0
- package/src/vendor/pi-tui/components/scroll-view.ts +224 -0
- package/src/vendor/pi-tui/components/select-list.ts +273 -0
- package/src/vendor/pi-tui/components/settings-list.ts +328 -0
- package/src/vendor/pi-tui/components/spacer.ts +28 -0
- package/src/vendor/pi-tui/components/stack.ts +154 -0
- package/src/vendor/pi-tui/components/text.ts +107 -0
- package/src/vendor/pi-tui/components/truncated-text.ts +65 -0
- package/src/vendor/pi-tui/components/v-stack.ts +33 -0
- package/src/vendor/pi-tui/editor-component.ts +74 -0
- package/src/vendor/pi-tui/fuzzy.ts +138 -0
- package/src/vendor/pi-tui/index.ts +156 -0
- package/src/vendor/pi-tui/keybindings.ts +320 -0
- package/src/vendor/pi-tui/keys.ts +1401 -0
- package/src/vendor/pi-tui/kill-ring.ts +46 -0
- package/src/vendor/pi-tui/latex.ts +1506 -0
- package/src/vendor/pi-tui/layout-node.ts +51 -0
- package/src/vendor/pi-tui/layout.ts +449 -0
- package/src/vendor/pi-tui/native-modifiers.ts +13 -0
- package/src/vendor/pi-tui/native-module-path.ts +31 -0
- package/src/vendor/pi-tui/native-platform.ts +63 -0
- package/src/vendor/pi-tui/stdin-buffer.ts +444 -0
- package/src/vendor/pi-tui/terminal-colors.ts +73 -0
- package/src/vendor/pi-tui/terminal-image.ts +696 -0
- package/src/vendor/pi-tui/terminal.ts +547 -0
- package/src/vendor/pi-tui/tui-alt-screen.ts +1742 -0
- package/src/vendor/pi-tui/tui-main-screen.ts +655 -0
- package/src/vendor/pi-tui/tui.ts +1456 -0
- package/src/vendor/pi-tui/undo-stack.ts +28 -0
- package/src/vendor/pi-tui/utils.ts +1345 -0
- package/src/vendor/pi-tui/word-navigation.ts +117 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI Responses API -- the second place, beside openai.ts, that knows an
|
|
3
|
+
* OpenAI wire format.
|
|
4
|
+
*
|
|
5
|
+
* It exists for one reason: reasoning that survives a turn. Chat Completions
|
|
6
|
+
* returns no reasoning for OpenAI's models, so every request starts the
|
|
7
|
+
* model's thinking from nothing; OpenAI measured the difference at 73.9% to
|
|
8
|
+
* 78.2% on Tau-Bench Retail (GPT-5 prompting guide). Responses returns each
|
|
9
|
+
* reasoning item with `encrypted_content` when `store` is false, and accepts
|
|
10
|
+
* it back unchanged.
|
|
11
|
+
*
|
|
12
|
+
* Stateless on purpose. `previous_response_id` would keep the conversation on
|
|
13
|
+
* OpenAI's servers -- a second truth beside the log that no projection could
|
|
14
|
+
* recompute. Instead the whole reasoning item goes into the thinking block's
|
|
15
|
+
* opaque raw, and the projection replays it like any other block.
|
|
16
|
+
*
|
|
17
|
+
* Event handling follows pi-mono `packages/ai/src/api/openai-responses-shared.ts`
|
|
18
|
+
* (MIT, 6f75515); the transport and SSE framing are this project's own.
|
|
19
|
+
*/
|
|
20
|
+
import type { AssistantBlock } from "../core/events.ts";
|
|
21
|
+
import type { ContentBlock, FrozenPrefix, Message } from "../core/project.ts";
|
|
22
|
+
import { assertNever } from "../core/exhaustive.ts";
|
|
23
|
+
import type { OnDelta, Provider, ProviderConfig, ToolSchema, Turn } from "./provider.ts";
|
|
24
|
+
import { pushSse, HttpError, StreamError } from "./openai.ts";
|
|
25
|
+
import { withRetry, DEFAULT_RETRY, type RetryPolicy } from "./retry.ts";
|
|
26
|
+
|
|
27
|
+
/** What this adapter parks in a thinking block's raw: the reasoning item, whole. */
|
|
28
|
+
interface ResponsesRaw { responses_item: Record<string, unknown> }
|
|
29
|
+
|
|
30
|
+
function responsesItemOf(raw: unknown): Record<string, unknown> | null {
|
|
31
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
32
|
+
const item = (raw as Partial<ResponsesRaw>).responses_item;
|
|
33
|
+
return typeof item === "object" && item !== null ? item : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type InputItem = Record<string, unknown>;
|
|
37
|
+
|
|
38
|
+
function inputItems(messages: readonly Message[]): InputItem[] {
|
|
39
|
+
const out: InputItem[] = [];
|
|
40
|
+
for (const message of messages) {
|
|
41
|
+
if (message.role === "assistant") {
|
|
42
|
+
const text: string[] = [];
|
|
43
|
+
const flushText = () => {
|
|
44
|
+
if (text.length === 0) return;
|
|
45
|
+
out.push({ type: "message", role: "assistant", content: [{ type: "output_text", text: text.join("") }] });
|
|
46
|
+
text.length = 0;
|
|
47
|
+
};
|
|
48
|
+
for (const block of message.content) {
|
|
49
|
+
switch (block.type) {
|
|
50
|
+
case "thinking": {
|
|
51
|
+
// Another adapter's raw (a Completions reasoning string) is not a
|
|
52
|
+
// Responses item and cannot be sent as one; a model switch cuts,
|
|
53
|
+
// so this is the rare replay of a mixed log, not the common path.
|
|
54
|
+
const item = responsesItemOf(block.raw);
|
|
55
|
+
if (item !== null) { flushText(); out.push(item); }
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
case "text":
|
|
59
|
+
text.push(block.text);
|
|
60
|
+
break;
|
|
61
|
+
case "tool_use": {
|
|
62
|
+
flushText();
|
|
63
|
+
const wire = typeof block.wire === "object" && block.wire !== null ? block.wire as Record<string, unknown> : {};
|
|
64
|
+
out.push({
|
|
65
|
+
type: "function_call",
|
|
66
|
+
...(typeof wire["item_id"] === "string" ? { id: wire["item_id"] } : {}),
|
|
67
|
+
call_id: block.id, name: block.name, arguments: JSON.stringify(block.input),
|
|
68
|
+
});
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
case "tool_result":
|
|
72
|
+
break;
|
|
73
|
+
default:
|
|
74
|
+
return assertNever(block, "responses assistant input");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
flushText();
|
|
78
|
+
} else {
|
|
79
|
+
const text: string[] = [];
|
|
80
|
+
for (const block of message.content as readonly ContentBlock[]) {
|
|
81
|
+
if (block.type === "text") text.push(block.text);
|
|
82
|
+
else if (block.type === "tool_result") {
|
|
83
|
+
out.push({ type: "function_call_output", call_id: block.tool_use_id,
|
|
84
|
+
output: block.is_error ? `[error] ${block.content}` : block.content });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (text.length > 0) out.push({ type: "message", role: "user", content: [{ type: "input_text", text: text.join("\n") }] });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function responsesTools(tools: readonly unknown[]): unknown[] {
|
|
94
|
+
return (tools as ToolSchema[]).map((tool) => ({
|
|
95
|
+
type: "function", name: tool.name, description: tool.description, parameters: tool.input_schema,
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function buildResponsesRequest(config: ProviderConfig, frozen: FrozenPrefix, messages: readonly Message[]): Record<string, unknown> {
|
|
100
|
+
const spec = config.spec;
|
|
101
|
+
const request: Record<string, unknown> = {
|
|
102
|
+
model: spec.id,
|
|
103
|
+
instructions: frozen.system,
|
|
104
|
+
input: inputItems(messages),
|
|
105
|
+
store: false,
|
|
106
|
+
max_output_tokens: config.maxTokens,
|
|
107
|
+
};
|
|
108
|
+
if (frozen.tools.length > 0) request["tools"] = responsesTools(frozen.tools);
|
|
109
|
+
if (spec.reasoning) {
|
|
110
|
+
// Accepted but no longer required by current models; older ones need it.
|
|
111
|
+
request["include"] = ["reasoning.encrypted_content"];
|
|
112
|
+
const mapped = spec.thinkingLevelMap?.[config.effort];
|
|
113
|
+
if (mapped !== null) request["reasoning"] = { effort: mapped ?? config.effort, summary: "auto" };
|
|
114
|
+
}
|
|
115
|
+
return request;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
interface Slot { kind: "thinking" | "text" | "call"; text: string; item?: Record<string, unknown> }
|
|
119
|
+
|
|
120
|
+
interface ResponseEnvelope {
|
|
121
|
+
status?: string;
|
|
122
|
+
incomplete_details?: { reason?: string } | null;
|
|
123
|
+
error?: { code?: string; message?: string } | null;
|
|
124
|
+
usage?: {
|
|
125
|
+
input_tokens?: number; output_tokens?: number;
|
|
126
|
+
input_tokens_details?: { cached_tokens?: number };
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function stopReasonOf(response: ResponseEnvelope, hasCalls: boolean): string | null {
|
|
131
|
+
switch (response.status) {
|
|
132
|
+
case "completed": return hasCalls ? "tool_use" : "end_turn";
|
|
133
|
+
// The loop's recovery keys on max_tokens; any other reason is not a
|
|
134
|
+
// completion and must not execute tools from the partial response.
|
|
135
|
+
case "incomplete": return response.incomplete_details?.reason === "max_output_tokens" ? "max_tokens" : `incomplete:${response.incomplete_details?.reason ?? "unknown"}`;
|
|
136
|
+
default: return response.status ?? null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function decodeArguments(raw: string): unknown {
|
|
141
|
+
if (raw.length === 0) return {};
|
|
142
|
+
try { return JSON.parse(raw); } catch { return { __unparsed_arguments: raw }; }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function readResponsesSse(body: ReadableStream<Uint8Array>, node: number, started: number, onDelta?: OnDelta): Promise<Turn> {
|
|
146
|
+
const slots = new Map<number, Slot>();
|
|
147
|
+
let final: ResponseEnvelope | undefined;
|
|
148
|
+
const handle = (event: Record<string, unknown>) => {
|
|
149
|
+
const index = typeof event["output_index"] === "number" ? event["output_index"] : -1;
|
|
150
|
+
const item = event["item"] as Record<string, unknown> | undefined;
|
|
151
|
+
switch (event["type"]) {
|
|
152
|
+
case "response.output_item.added": {
|
|
153
|
+
const kind = item?.["type"] === "reasoning" ? "thinking" : item?.["type"] === "function_call" ? "call" : item?.["type"] === "message" ? "text" : null;
|
|
154
|
+
if (kind !== null) slots.set(index, { kind, text: "", ...(item ? { item } : {}) });
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
case "response.reasoning_summary_text.delta":
|
|
158
|
+
case "response.reasoning_text.delta":
|
|
159
|
+
case "response.output_text.delta":
|
|
160
|
+
case "response.refusal.delta":
|
|
161
|
+
case "response.function_call_arguments.delta": {
|
|
162
|
+
const slot = slots.get(index);
|
|
163
|
+
const delta = typeof event["delta"] === "string" ? event["delta"] : "";
|
|
164
|
+
if (slot === undefined || delta.length === 0) return;
|
|
165
|
+
slot.text += delta;
|
|
166
|
+
if (slot.kind === "thinking") onDelta?.({ kind: "thinking", text: delta });
|
|
167
|
+
else if (slot.kind === "text") onDelta?.({ kind: "text", text: delta });
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
case "response.output_item.done": {
|
|
171
|
+
if (item === undefined) return;
|
|
172
|
+
const slot = slots.get(index) ?? { kind: item["type"] === "reasoning" ? "thinking" : item["type"] === "function_call" ? "call" : "text", text: "" } as Slot;
|
|
173
|
+
slot.item = item;
|
|
174
|
+
if (item["type"] === "function_call" && typeof item["arguments"] === "string") slot.text = item["arguments"];
|
|
175
|
+
if (item["type"] === "message" && Array.isArray(item["content"])) {
|
|
176
|
+
const parts = (item["content"] as Record<string, unknown>[]).map((c) => typeof c["text"] === "string" ? c["text"] : typeof c["refusal"] === "string" ? c["refusal"] : "");
|
|
177
|
+
if (parts.join("").length > 0) slot.text = parts.join("");
|
|
178
|
+
}
|
|
179
|
+
slots.set(index, slot);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
case "response.completed":
|
|
183
|
+
case "response.incomplete":
|
|
184
|
+
final = event["response"] as ResponseEnvelope;
|
|
185
|
+
return;
|
|
186
|
+
case "response.failed": {
|
|
187
|
+
const response = event["response"] as ResponseEnvelope | undefined;
|
|
188
|
+
const error = response?.error;
|
|
189
|
+
throw new StreamError(error ? `${error.code ?? "error"}: ${error.message ?? "no message"}` : "response failed without details", error?.code);
|
|
190
|
+
}
|
|
191
|
+
case "error":
|
|
192
|
+
throw new StreamError(`stream error: ${String(event["message"] ?? event["code"] ?? "unknown")}`,
|
|
193
|
+
typeof event["code"] === "string" ? event["code"] : undefined);
|
|
194
|
+
default:
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const reader = body.getReader();
|
|
200
|
+
const decoder = new TextDecoder();
|
|
201
|
+
let buffer = "";
|
|
202
|
+
try {
|
|
203
|
+
for (;;) {
|
|
204
|
+
const { value, done } = await reader.read();
|
|
205
|
+
const pushed = pushSse(buffer, decoder.decode(value ?? new Uint8Array(), { stream: !done }));
|
|
206
|
+
buffer = pushed.buffer;
|
|
207
|
+
for (const payload of pushed.payloads) if (payload.length > 0 && payload !== "[DONE]") handle(JSON.parse(payload));
|
|
208
|
+
if (done) break;
|
|
209
|
+
}
|
|
210
|
+
for (const payload of pushSse(buffer, "\n").payloads) if (payload.length > 0 && payload !== "[DONE]") handle(JSON.parse(payload));
|
|
211
|
+
} finally { reader.releaseLock(); }
|
|
212
|
+
if (final === undefined) throw new Error("responses stream ended before a terminal response event");
|
|
213
|
+
|
|
214
|
+
const blocks: AssistantBlock[] = [];
|
|
215
|
+
for (const [, slot] of [...slots.entries()].sort(([a], [b]) => a - b)) {
|
|
216
|
+
if (slot.kind === "thinking" && slot.item !== undefined) {
|
|
217
|
+
const summary = Array.isArray(slot.item["summary"])
|
|
218
|
+
? (slot.item["summary"] as Record<string, unknown>[]).map((s) => String(s["text"] ?? "")).join("\n\n") : "";
|
|
219
|
+
blocks.push({ type: "thinking", text: summary || slot.text, raw: { responses_item: slot.item } satisfies ResponsesRaw });
|
|
220
|
+
} else if (slot.kind === "text" && slot.text.length > 0) {
|
|
221
|
+
blocks.push({ type: "text", text: slot.text });
|
|
222
|
+
} else if (slot.kind === "call" && slot.item !== undefined) {
|
|
223
|
+
const callId = typeof slot.item["call_id"] === "string" ? slot.item["call_id"] : `call_${blocks.length}`;
|
|
224
|
+
blocks.push({ type: "tool_use", call_id: callId, name: String(slot.item["name"] ?? ""), input: decodeArguments(slot.text),
|
|
225
|
+
...(typeof slot.item["id"] === "string" ? { wire: { item_id: slot.item["id"] } } : {}) });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const usage = final.usage;
|
|
229
|
+
const cached = usage?.input_tokens_details?.cached_tokens ?? 0;
|
|
230
|
+
return {
|
|
231
|
+
blocks,
|
|
232
|
+
stopReason: stopReasonOf(final, blocks.some((block) => block.type === "tool_use")),
|
|
233
|
+
usage: {
|
|
234
|
+
// input_tokens includes the cached part; the uncached remainder is the difference.
|
|
235
|
+
input_tokens: Math.max(0, (usage?.input_tokens ?? 0) - cached),
|
|
236
|
+
output_tokens: usage?.output_tokens ?? 0,
|
|
237
|
+
cache_creation_input_tokens: 0,
|
|
238
|
+
cache_read_input_tokens: cached,
|
|
239
|
+
node, ms: Date.now() - started,
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export class OpenAIResponsesProvider implements Provider {
|
|
245
|
+
readonly config: ProviderConfig;
|
|
246
|
+
private readonly apiKey: string;
|
|
247
|
+
private readonly fetchImpl: typeof fetch;
|
|
248
|
+
private readonly retry: RetryPolicy;
|
|
249
|
+
private readonly onRetry: ((attempt: number, delayMs: number, error: unknown) => void) | undefined;
|
|
250
|
+
|
|
251
|
+
constructor(config: ProviderConfig, apiKey: string, options: {
|
|
252
|
+
fetchImpl?: typeof fetch; retry?: RetryPolicy; onRetry?: (attempt: number, delayMs: number, error: unknown) => void;
|
|
253
|
+
} = {}) {
|
|
254
|
+
this.config = config;
|
|
255
|
+
this.apiKey = apiKey;
|
|
256
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
257
|
+
this.retry = options.retry ?? DEFAULT_RETRY;
|
|
258
|
+
this.onRetry = options.onRetry;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
send(frozen: FrozenPrefix, messages: readonly Message[], node: number, signal?: AbortSignal, onDelta?: OnDelta): Promise<Turn> {
|
|
262
|
+
return withRetry(() => this.attempt(frozen, messages, node, signal, onDelta), this.retry, undefined,
|
|
263
|
+
(attempt, delayMs, error) => { onDelta?.({ kind: "discard" }); this.onRetry?.(attempt, delayMs, error); }, signal);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private async attempt(frozen: FrozenPrefix, messages: readonly Message[], node: number, signal?: AbortSignal, onDelta?: OnDelta): Promise<Turn> {
|
|
267
|
+
const started = Date.now();
|
|
268
|
+
const baseUrl = this.config.spec.baseUrl;
|
|
269
|
+
if (baseUrl === undefined) throw new Error(`model ${this.config.spec.id} has no baseUrl to send to`);
|
|
270
|
+
const init: RequestInit = {
|
|
271
|
+
method: "POST",
|
|
272
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${this.apiKey}` },
|
|
273
|
+
body: JSON.stringify({ ...buildResponsesRequest(this.config, frozen, messages), stream: true }),
|
|
274
|
+
};
|
|
275
|
+
if (signal !== undefined) init.signal = signal;
|
|
276
|
+
const response = await this.fetchImpl(`${baseUrl.replace(/\/$/, "")}/responses`, init);
|
|
277
|
+
if (!response.ok) {
|
|
278
|
+
const text = await response.text();
|
|
279
|
+
let detail = text.slice(0, 200);
|
|
280
|
+
try { const parsed = JSON.parse(text) as { error?: { message?: string } }; if (parsed.error?.message) detail = parsed.error.message; } catch { /* raw body */ }
|
|
281
|
+
throw new HttpError(response.status, response.headers, `HTTP ${response.status}: ${detail}`);
|
|
282
|
+
}
|
|
283
|
+
if (response.body === null) throw new Error(`${baseUrl} returned a streaming 200 with no body`);
|
|
284
|
+
return readResponsesSse(response.body, node, started, onDelta);
|
|
285
|
+
}
|
|
286
|
+
}
|