@volter/twin-openai 0.1.1 → 2.0.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.
Files changed (120) hide show
  1. package/README.md +33 -30
  2. package/defaults/handlers.json +10 -0
  3. package/dist/defaults/handlers.json +10 -0
  4. package/dist/src/cli.d.ts +2 -0
  5. package/dist/src/cli.js +29 -0
  6. package/dist/src/generated/surface.gen.json +1 -0
  7. package/dist/src/generated/ui.gen.json +1 -0
  8. package/dist/src/index.d.ts +19 -0
  9. package/dist/src/index.js +72 -0
  10. package/dist/src/manifest.d.ts +6 -0
  11. package/dist/src/manifest.js +323 -0
  12. package/dist/src/openai-budget.d.ts +53 -0
  13. package/dist/src/openai-budget.js +147 -0
  14. package/dist/src/openai-capabilities.d.ts +4 -0
  15. package/dist/src/openai-capabilities.js +1569 -0
  16. package/dist/src/openai-conformance.d.ts +13 -0
  17. package/dist/src/openai-conformance.js +116 -0
  18. package/dist/src/openai-connector.d.ts +86 -0
  19. package/dist/src/openai-connector.js +291 -0
  20. package/dist/src/openai-media.d.ts +43 -0
  21. package/dist/src/openai-media.js +257 -0
  22. package/dist/src/openai-models.d.ts +74 -0
  23. package/dist/src/openai-models.js +148 -0
  24. package/dist/src/openai-scenario.d.ts +51 -0
  25. package/dist/src/openai-scenario.js +166 -0
  26. package/dist/src/openai-server.d.ts +40 -0
  27. package/dist/src/openai-server.js +126 -0
  28. package/dist/src/openai-stub.d.ts +82 -0
  29. package/dist/src/openai-stub.js +256 -0
  30. package/dist/src/openai-twin.d.ts +182 -0
  31. package/dist/src/openai-twin.js +1117 -0
  32. package/dist/src/openai-types.d.ts +194 -0
  33. package/dist/src/openai-types.js +4 -0
  34. package/dist/src/openai-webhooks.d.ts +47 -0
  35. package/dist/src/openai-webhooks.js +99 -0
  36. package/dist/src/screens/api-keys.d.ts +16 -0
  37. package/dist/src/screens/api-keys.js +131 -0
  38. package/dist/src/screens/session.d.ts +22 -0
  39. package/dist/src/screens/session.js +115 -0
  40. package/dist/src/semantics/assistants.d.ts +2 -0
  41. package/dist/src/semantics/assistants.js +331 -0
  42. package/dist/src/semantics/audio.d.ts +2 -0
  43. package/dist/src/semantics/audio.js +27 -0
  44. package/dist/src/semantics/batches.d.ts +4 -0
  45. package/dist/src/semantics/batches.js +86 -0
  46. package/dist/src/semantics/chat-completions.d.ts +3 -0
  47. package/dist/src/semantics/chat-completions.js +58 -0
  48. package/dist/src/semantics/containers.d.ts +2 -0
  49. package/dist/src/semantics/containers.js +147 -0
  50. package/dist/src/semantics/embeddings.d.ts +2 -0
  51. package/dist/src/semantics/embeddings.js +13 -0
  52. package/dist/src/semantics/evals.d.ts +2 -0
  53. package/dist/src/semantics/evals.js +173 -0
  54. package/dist/src/semantics/files.d.ts +13 -0
  55. package/dist/src/semantics/files.js +59 -0
  56. package/dist/src/semantics/fine-tuning.d.ts +4 -0
  57. package/dist/src/semantics/fine-tuning.js +178 -0
  58. package/dist/src/semantics/images.d.ts +2 -0
  59. package/dist/src/semantics/images.js +18 -0
  60. package/dist/src/semantics/index.d.ts +8 -0
  61. package/dist/src/semantics/index.js +46 -0
  62. package/dist/src/semantics/models.d.ts +2 -0
  63. package/dist/src/semantics/models.js +34 -0
  64. package/dist/src/semantics/moderations.d.ts +2 -0
  65. package/dist/src/semantics/moderations.js +12 -0
  66. package/dist/src/semantics/organization.d.ts +2 -0
  67. package/dist/src/semantics/organization.js +67 -0
  68. package/dist/src/semantics/progress.d.ts +22 -0
  69. package/dist/src/semantics/progress.js +63 -0
  70. package/dist/src/semantics/responses.d.ts +3 -0
  71. package/dist/src/semantics/responses.js +153 -0
  72. package/dist/src/semantics/shared.d.ts +32 -0
  73. package/dist/src/semantics/shared.js +69 -0
  74. package/dist/src/semantics/uploads.d.ts +2 -0
  75. package/dist/src/semantics/uploads.js +84 -0
  76. package/dist/src/semantics/vector-stores.d.ts +2 -0
  77. package/dist/src/semantics/vector-stores.js +281 -0
  78. package/dist/test-fixtures/openai-openapi-operations.SOURCE.md +18 -0
  79. package/dist/test-fixtures/openai-openapi-operations.json +1849 -0
  80. package/package.json +21 -10
  81. package/src/cli.ts +9 -7
  82. package/src/generated/surface.gen.json +1 -0
  83. package/src/generated/ui.gen.json +1 -0
  84. package/src/index.ts +20 -10
  85. package/src/manifest.ts +343 -0
  86. package/src/openai-budget.ts +4 -4
  87. package/src/openai-capabilities.ts +177 -195
  88. package/src/openai-conformance.ts +1 -1
  89. package/src/openai-connector.ts +40 -43
  90. package/src/openai-media.ts +225 -0
  91. package/src/openai-models.ts +145 -15
  92. package/src/openai-scenario.ts +46 -10
  93. package/src/openai-server.ts +65 -108
  94. package/src/openai-stub.ts +54 -30
  95. package/src/openai-twin.ts +760 -1665
  96. package/src/openai-types.ts +24 -6
  97. package/src/openai-webhooks.ts +2 -1
  98. package/src/screens/api-keys.tsx +138 -0
  99. package/src/screens/session.tsx +131 -0
  100. package/src/semantics/assistants.ts +336 -0
  101. package/src/semantics/audio.ts +31 -0
  102. package/src/semantics/batches.ts +88 -0
  103. package/src/semantics/chat-completions.ts +66 -0
  104. package/src/semantics/containers.ts +151 -0
  105. package/src/semantics/embeddings.ts +19 -0
  106. package/src/semantics/evals.ts +182 -0
  107. package/src/semantics/files.ts +67 -0
  108. package/src/semantics/fine-tuning.ts +185 -0
  109. package/src/semantics/images.ts +23 -0
  110. package/src/semantics/index.ts +52 -0
  111. package/src/semantics/models.ts +41 -0
  112. package/src/semantics/moderations.ts +14 -0
  113. package/src/semantics/organization.ts +76 -0
  114. package/src/semantics/progress.ts +72 -0
  115. package/src/semantics/responses.ts +151 -0
  116. package/src/semantics/shared.ts +82 -0
  117. package/src/semantics/uploads.ts +92 -0
  118. package/src/semantics/vector-stores.ts +279 -0
  119. package/test-fixtures/openai-openapi-operations.SOURCE.md +4 -5
  120. package/test-fixtures/openai-openapi-operations.json +224 -1334
@@ -0,0 +1,331 @@
1
+ import { estimateTokens, stubToolCall } from "../openai-stub.js";
2
+ import { coreAnswer, progress } from "./progress.js";
3
+ import { at, epoch, invalid, missing, objectOr, pick } from "./shared.js";
4
+ const ASSISTANT = 'AssistantObject';
5
+ const THREAD = 'ThreadObject';
6
+ const MESSAGE = 'MessageObject';
7
+ const RUN = 'RunObject';
8
+ // ── assistants ──────────────────────────────────────────────────────────────────────────────
9
+ /** An assistant's tool resources: as sent, with the empty store list a file_search tool searches when none is named
10
+ * (the reference's modify example answers `"tool_resources": {"file_search": {"vector_store_ids": []}}` for an
11
+ * assistant given the file_search tool and no stores: https://platform.openai.com/docs/api-reference/assistants/modifyAssistant). */
12
+ function toolResources(tools, sent) {
13
+ const out = { ...(sent && typeof sent === 'object' ? sent : {}) };
14
+ if (Array.isArray(tools) && tools.some((t) => t?.type === 'file_search') && !out.file_search)
15
+ out.file_search = { vector_store_ids: [] };
16
+ // and the empty file list a code interpreter works on (the reference's listRuns and getThread examples answer
17
+ // `"code_interpreter": {"file_ids": []}`)
18
+ if (Array.isArray(tools) && tools.some((t) => t?.type === 'code_interpreter') && !out.code_interpreter)
19
+ out.code_interpreter = { file_ids: [] };
20
+ return out;
21
+ }
22
+ const createAssistant = async (ctx) => {
23
+ const p = ctx.params;
24
+ if (p.model === undefined || p.model === '')
25
+ return invalid(ctx, 'you must provide a model parameter', 'model');
26
+ const tools = Array.isArray(p.tools) ? p.tools : [];
27
+ const fields = {
28
+ object: 'assistant', created_at: epoch(ctx),
29
+ name: p.name ?? null, description: p.description ?? null,
30
+ model: String(p.model), instructions: p.instructions ?? null,
31
+ tools,
32
+ tool_resources: toolResources(tools, p.tool_resources),
33
+ metadata: objectOr(p.metadata, {}),
34
+ // sampling "Defaults to 1" (https://platform.openai.com/docs/api-reference/assistants/object, `temperature`, `top_p`)
35
+ temperature: p.temperature ?? 1, top_p: p.top_p ?? 1,
36
+ response_format: p.response_format ?? 'auto',
37
+ };
38
+ return ctx.reply(await ctx.write(ASSISTANT, ctx.mint(ASSISTANT), fields, 'assistant.create'));
39
+ };
40
+ // modify replaces only the fields the request names
41
+ const modifyAssistant = async (ctx) => {
42
+ const id = String(ctx.id);
43
+ const current = ctx.get(ASSISTANT, id);
44
+ if (!current)
45
+ return ctx.notFound(ASSISTANT, id);
46
+ const fields = pick(ctx.params, ['name', 'description', 'model', 'instructions', 'tools', 'tool_resources', 'metadata', 'temperature', 'top_p', 'response_format']);
47
+ if (fields.tools !== undefined || fields.tool_resources !== undefined)
48
+ fields.tool_resources = toolResources(fields.tools ?? current.tools, fields.tool_resources ?? current.tool_resources);
49
+ return ctx.reply(await ctx.write(ASSISTANT, id, fields, 'assistant.update'));
50
+ };
51
+ // ── threads and messages ────────────────────────────────────────────────────────────────────
52
+ /** The id of the next message appended to a thread: the thread's next sequence number. */
53
+ const nextMessage = (ctx, threadId) => {
54
+ const seq = ctx.rowsRaw(MESSAGE, { withDeleted: true }).filter((r) => r.thread_id === threadId).length + 1;
55
+ return { seq, id: `msg-twin-${threadId}-${seq}` };
56
+ };
57
+ /** A message appended to a thread, named by the thread's next sequence number (or the id a run
58
+ * reserved for its reply). */
59
+ async function append(ctx, threadId, p, reserved) {
60
+ const next = nextMessage(ctx, threadId);
61
+ const id = reserved ?? next.id;
62
+ const seq = reserved ? Number(reserved.split('-').at(-1)) : next.seq;
63
+ const text = typeof p.content === 'string'
64
+ ? p.content
65
+ : Array.isArray(p.content) ? p.content.map((part) => String(part.text?.value ?? part.text ?? '')).join('') : '';
66
+ const fields = {
67
+ object: 'thread.message', created_at: epoch(ctx), thread_id: threadId, role: typeof p.role === 'string' ? p.role : 'user',
68
+ // a message added to a thread is whole when it is added
69
+ status: 'completed', completed_at: epoch(ctx), incomplete_at: null, incomplete_details: null,
70
+ content: [{ type: 'text', text: { value: text, annotations: [] } }],
71
+ assistant_id: p.assistant_id ?? null, run_id: p.run_id ?? null,
72
+ attachments: Array.isArray(p.attachments) ? p.attachments : [],
73
+ metadata: objectOr(p.metadata, {}),
74
+ _seq: seq,
75
+ };
76
+ return ctx.write(MESSAGE, id, fields, 'message.create');
77
+ }
78
+ /** The thread the path names (a deleted one still holds its messages and runs), or OpenAI's answer. */
79
+ function thread(ctx) {
80
+ const id = at(ctx, 'thread_id');
81
+ return ctx.row(THREAD, id, { withDeleted: true }) ? { id } : { answer: ctx.notFound(THREAD, id) };
82
+ }
83
+ const createThread = async (ctx) => {
84
+ const p = ctx.params;
85
+ const id = ctx.mint(THREAD);
86
+ // a thread made with no tool resources has none: `{}` (the reference's createThread examples)
87
+ const body = await ctx.write(THREAD, id, { object: 'thread', created_at: epoch(ctx), metadata: objectOr(p.metadata, {}), tool_resources: objectOr(p.tool_resources, {}) }, 'thread.create');
88
+ // messages given at creation are the thread's first
89
+ if (Array.isArray(p.messages))
90
+ for (const m of p.messages)
91
+ await append(ctx, id, m);
92
+ return ctx.reply(body);
93
+ };
94
+ const modifyThread = async (ctx) => {
95
+ const id = String(ctx.id);
96
+ if (!ctx.get(THREAD, id))
97
+ return ctx.notFound(THREAD, id);
98
+ return ctx.reply(await ctx.write(THREAD, id, pick(ctx.params, ['metadata', 'tool_resources']), 'thread.update'));
99
+ };
100
+ const createMessage = async (ctx) => {
101
+ const found = thread(ctx);
102
+ if ('answer' in found)
103
+ return found.answer;
104
+ if (ctx.params.content === undefined)
105
+ return invalid(ctx, 'you must provide a content parameter', 'content');
106
+ return ctx.reply(await append(ctx, found.id, ctx.params));
107
+ };
108
+ // ── runs and steps ──────────────────────────────────────────────────────────────────────────
109
+ /** The run the path names under its thread, raw (its reply message is bookkeeping), or OpenAI's answer. */
110
+ function run(ctx) {
111
+ const id = at(ctx, 'run_id');
112
+ const found = ctx.row(RUN, id);
113
+ return found && found.thread_id === at(ctx, 'thread_id') ? { run: found } : { answer: ctx.notFound(RUN, id) };
114
+ }
115
+ const replyText = (model) => `[twin-stub:${model}] deterministic assistant run output (no model weights are run)`;
116
+ /** What a completed run carries: its reply message and usage; a run that waited on its tools keeps when it started. */
117
+ function finishRun(ctx) {
118
+ return (row, end) => {
119
+ if (end !== 'completed')
120
+ return {};
121
+ const text = replyText(String(row.model));
122
+ return { _reply_msg: nextMessage(ctx, String(row.thread_id)).id, expires_at: null, ...(row.started_at ? { started_at: row.started_at } : {}), usage: { prompt_tokens: 0, completion_tokens: estimateTokens(text), total_tokens: estimateTokens(text) } };
123
+ };
124
+ }
125
+ /** The function tools a run's model calls: its placeholder decision is to call them (each one, or the one
126
+ * `tool_choice` names, or the first when calls are not parallel), unless `tool_choice` is `none` or their outputs
127
+ * were already submitted. */
128
+ function toolCalls(r, userText = '') {
129
+ const functions = (Array.isArray(r.tools) ? r.tools : []).filter((t) => t.type === 'function');
130
+ if (!functions.length || r.tool_choice === 'none' || r._tools_answered)
131
+ return [];
132
+ const named = r.tool_choice && typeof r.tool_choice === 'object' ? String(r.tool_choice.function?.name ?? '') : '';
133
+ const chosen = named ? functions.filter((t) => t.function?.name === named) : r.parallel_tool_calls === false ? functions.slice(0, 1) : functions;
134
+ const run = String(r.id).replace(/[^A-Za-z0-9]+/g, '_');
135
+ return chosen.map((tool, i) => {
136
+ const call = stubToolCall([tool], i + 1, undefined, userText);
137
+ return { id: `call_${run}_${i + 1}`, type: 'function', function: call.function };
138
+ });
139
+ }
140
+ /** A run whose model calls its function tools stops at `requires_action`, naming the calls it waits on
141
+ * (https://platform.openai.com/docs/assistants/tools/function-calling: "the Run will enter a requires_action
142
+ * status"); the vendor's moves, decided and written at once. */
143
+ async function waitOnTools(ctx, id) {
144
+ return ctx.atomically((rows) => {
145
+ const current = rows(RUN).find((r) => r.id === id);
146
+ if (!current || current.status !== 'queued')
147
+ return { value: false };
148
+ // the thread's last user message is what the model answers, and what its calls take their values from
149
+ const asked = rows(MESSAGE).filter((m) => m.thread_id === current.thread_id && m.role === 'user').sort((a, b) => Number(a._seq ?? 0) - Number(b._seq ?? 0)).at(-1);
150
+ const calls = toolCalls(current, String(asked?.content?.[0]?.text?.value ?? ''));
151
+ if (!calls.length)
152
+ return { value: false };
153
+ if (ctx.legal(RUN, 'status', ctx.call.operation.id, 'queued', 'in_progress', id, 'vendor') || ctx.legal(RUN, 'status', ctx.call.operation.id, 'in_progress', 'requires_action', id, 'vendor'))
154
+ return { value: false };
155
+ const fields = { status: 'requires_action', started_at: epoch(ctx), required_action: { type: 'submit_tool_outputs', submit_tool_outputs: { tool_calls: calls } }, _tool_calls: calls };
156
+ return { value: true, write: { resource: RUN, id, fields, operation: 'run.update' } };
157
+ });
158
+ }
159
+ /** Observe the vendor's work on a run: it stops for its function tools, else the read that completes it appends
160
+ * the reply to its thread. */
161
+ async function work(ctx, id) {
162
+ if (await waitOnTools(ctx, id))
163
+ return;
164
+ const moved = await progress(ctx, RUN, id, finishRun(ctx));
165
+ if (moved?.to !== 'completed')
166
+ return;
167
+ const r = moved.row;
168
+ await append(ctx, String(r.thread_id), { role: 'assistant', content: replyText(String(r.model)), assistant_id: r.assistant_id, run_id: r.id }, String(r._reply_msg));
169
+ }
170
+ /** A run's steps, newest first: the call of its function tools (done once their outputs are in, each call with its
171
+ * output), and, once it completed, the creation of its reply message. */
172
+ function steps(r) {
173
+ const base = { object: 'thread.run.step', run_id: String(r.id), assistant_id: String(r.assistant_id), thread_id: String(r.thread_id), cancelled_at: null, expired_at: null, failed_at: null, last_error: null, metadata: {} };
174
+ const out = [];
175
+ const calls = (r._tool_calls ?? []);
176
+ if (calls.length) {
177
+ const outputs = (r._tool_outputs ?? {});
178
+ const done = r._tools_answered === true;
179
+ const cancelled = !done && (r.status === 'cancelled' || r.status === 'cancelling');
180
+ out.push({
181
+ ...base, id: `step-${r.id}-1`, created_at: Number(r.started_at ?? r.created_at ?? 0), type: 'tool_calls',
182
+ status: done ? 'completed' : cancelled ? 'cancelled' : 'in_progress', completed_at: done ? Number(r._tools_answered_at ?? r.started_at ?? 0) : null,
183
+ ...(cancelled ? { cancelled_at: Number(r.cancelled_at ?? r.started_at ?? 0) } : {}),
184
+ step_details: { type: 'tool_calls', tool_calls: calls.map((c) => ({ ...c, function: { ...c.function, output: done ? (outputs[String(c.id)] ?? null) : null } })) },
185
+ usage: null,
186
+ });
187
+ }
188
+ if (r.status === 'completed') {
189
+ const created = Number(r.completed_at ?? r.created_at ?? 0);
190
+ out.unshift({
191
+ ...base, id: `step-${r.id}-${out.length + 1}`, created_at: created,
192
+ type: 'message_creation', status: 'completed', completed_at: created,
193
+ step_details: { type: 'message_creation', message_creation: { message_id: String(r._reply_msg ?? '') } },
194
+ usage: r.usage ?? null,
195
+ });
196
+ }
197
+ return out;
198
+ }
199
+ const createRun = async (ctx) => {
200
+ const found = thread(ctx);
201
+ if ('answer' in found)
202
+ return found.answer;
203
+ const p = ctx.params;
204
+ if (p.assistant_id === undefined || p.assistant_id === '')
205
+ return invalid(ctx, 'you must provide an assistant_id parameter', 'assistant_id');
206
+ const assistantId = String(p.assistant_id);
207
+ const assistant = ctx.get(ASSISTANT, assistantId);
208
+ if (!assistant)
209
+ return ctx.notFound(ASSISTANT, assistantId);
210
+ const id = ctx.mint(RUN);
211
+ const created = epoch(ctx);
212
+ const model = typeof p.model === 'string' && p.model ? p.model : String(assistant.model);
213
+ const fields = {
214
+ object: 'thread.run', created_at: created, thread_id: found.id, assistant_id: assistantId,
215
+ status: 'queued', model, instructions: p.instructions ?? assistant.instructions ?? null,
216
+ tools: Array.isArray(p.tools) ? p.tools : (assistant.tools ?? []),
217
+ // the tool resources it runs with, its assistant's (the reference's cancelRun example; spec/patches.json)
218
+ tool_resources: assistant.tool_resources ?? {},
219
+ // a run in flight expires ten minutes after it was created; a completed one no longer does (the reference's
220
+ // streamed run: `expires_at` 600 seconds past `created_at` until `thread.run.completed` answers it null)
221
+ started_at: null, completed_at: null, expires_at: created + 600, cancelled_at: null, failed_at: null,
222
+ required_action: null, last_error: null, usage: null, incomplete_details: null,
223
+ // the request's sampling, else the assistant's (https://platform.openai.com/docs/api-reference/runs/object)
224
+ temperature: p.temperature ?? assistant.temperature ?? 1, top_p: p.top_p ?? assistant.top_p ?? 1,
225
+ // the request's settings as sent, or OpenAI's defaults for a run
226
+ max_prompt_tokens: p.max_prompt_tokens ?? null, max_completion_tokens: p.max_completion_tokens ?? null,
227
+ parallel_tool_calls: p.parallel_tool_calls ?? true, tool_choice: p.tool_choice ?? 'auto',
228
+ response_format: p.response_format ?? assistant.response_format ?? 'auto', truncation_strategy: p.truncation_strategy ?? { type: 'auto', last_messages: null },
229
+ metadata: objectOr(p.metadata, {}),
230
+ };
231
+ const created_ = await ctx.write(RUN, id, fields, 'run.create');
232
+ return p.stream === true ? streamRun(ctx, created_) : ctx.reply(created_);
233
+ };
234
+ /** A run created with `stream: true` answers its events as it runs (https://platform.openai.com/docs/api-reference/assistants-streaming/events):
235
+ * created, queued and in progress, then either its tool-call step and `thread.run.requires_action` (a run that waits on
236
+ * its function tools), or its message-creation step, the reply message with its text as deltas, and the run completed;
237
+ * then `done`. Submitted tool outputs stream the same way from the finished tool-call step. The twin works the run in
238
+ * the stream as a read would; each event is a server-sent `event:` with its object as `data`. */
239
+ async function streamRun(ctx, queued, submitted = false) {
240
+ const id = String(queued.id);
241
+ await work(ctx, id);
242
+ const done = ctx.get(RUN, id);
243
+ const all = steps(ctx.row(RUN, id));
244
+ const toolStep = all.find((x) => x.type === 'tool_calls');
245
+ const step = all.find((x) => x.type === 'message_creation');
246
+ const message = step ? ctx.get(MESSAGE, String(step.step_details.message_creation.message_id)) : undefined;
247
+ const started = { ...done, status: 'in_progress', completed_at: null, required_action: null, usage: null };
248
+ const events = submitted
249
+ ? [['thread.run.step.completed', toolStep], ['thread.run.queued', queued], ['thread.run.in_progress', started]]
250
+ : [['thread.run.created', queued], ['thread.run.queued', queued], ['thread.run.in_progress', started]];
251
+ if (done.status === 'requires_action' && toolStep) {
252
+ events.push(['thread.run.step.created', toolStep], ['thread.run.step.in_progress', toolStep], ['thread.run.requires_action', done]);
253
+ }
254
+ else {
255
+ if (step && message) {
256
+ const text = String(message.content[0]?.text?.value ?? '');
257
+ const stepRunning = { ...step, status: 'in_progress', completed_at: null, usage: null };
258
+ const messageRunning = { ...message, status: 'in_progress', completed_at: null, content: [] };
259
+ events.push(['thread.run.step.created', stepRunning], ['thread.run.step.in_progress', stepRunning], ['thread.message.created', messageRunning], ['thread.message.in_progress', messageRunning]);
260
+ for (let i = 0; i < text.length; i += 20)
261
+ events.push(['thread.message.delta', { id: message.id, object: 'thread.message.delta', delta: { content: [{ index: 0, type: 'text', text: { value: text.slice(i, i + 20), annotations: [] } }] } }]);
262
+ events.push(['thread.message.completed', message], ['thread.run.step.completed', step]);
263
+ }
264
+ events.push(['thread.run.completed', done]);
265
+ }
266
+ const body = events.map(([event, data]) => `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`).join('') + 'event: done\ndata: [DONE]\n\n';
267
+ return ctx.raw(body, { headers: { 'content-type': 'text/event-stream; charset=utf-8', 'cache-control': 'no-cache' } });
268
+ }
269
+ /** Submitting a waiting run's tool outputs queues it again to finish (https://platform.openai.com/docs/api-reference/runs/submitToolOutputs):
270
+ * only a run at `requires_action` takes them, and they must answer every call it waits on, and only those. */
271
+ const submitToolOutputs = async (ctx) => {
272
+ const found = run(ctx);
273
+ if ('answer' in found)
274
+ return found.answer;
275
+ const r = found.run;
276
+ const refusal = ctx.legal(RUN, 'status', 'submitToolOuputsToRun', r.status, 'queued');
277
+ if (refusal)
278
+ return ctx.refuse(refusal);
279
+ const outputs = Array.isArray(ctx.params.tool_outputs) ? ctx.params.tool_outputs : undefined;
280
+ if (!outputs)
281
+ return invalid(ctx, 'you must provide a tool_outputs array', 'tool_outputs');
282
+ const waiting = (r._tool_calls ?? []).map((c) => String(c.id));
283
+ const given = outputs.map((o) => String(o.tool_call_id ?? ''));
284
+ const unknown = given.find((g) => !waiting.includes(g));
285
+ if (unknown !== undefined)
286
+ return invalid(ctx, `Invalid tool_call_id: ${unknown}. No tool call with that id is waiting on this run.`, 'tool_outputs');
287
+ const missingIds = waiting.filter((w) => !given.includes(w));
288
+ if (missingIds.length)
289
+ return invalid(ctx, `Expected tool outputs for call_ids ${JSON.stringify(waiting)}, got ${JSON.stringify(given)}`, 'tool_outputs');
290
+ const fields = { status: 'queued', required_action: null, _tools_answered: true, _tools_answered_at: epoch(ctx), _tool_outputs: Object.fromEntries(outputs.map((o) => [String(o.tool_call_id), String(o.output ?? '')])) };
291
+ const queued = await ctx.write(RUN, String(r.id), fields, 'run.update');
292
+ return ctx.params.stream === true ? streamRun(ctx, queued, true) : ctx.reply(queued);
293
+ };
294
+ const listRunSteps = async (ctx) => {
295
+ await work(ctx, at(ctx, 'run_id'));
296
+ const found = run(ctx);
297
+ if ('answer' in found)
298
+ return found.answer;
299
+ const data = steps(found.run);
300
+ return ctx.reply({ object: 'list', data, has_more: false, first_id: data[0]?.id ?? null, last_id: data.at(-1)?.id ?? null });
301
+ };
302
+ const getRunStep = async (ctx) => {
303
+ await work(ctx, at(ctx, 'run_id'));
304
+ const found = run(ctx);
305
+ if ('answer' in found)
306
+ return found.answer;
307
+ const id = at(ctx, 'step_id');
308
+ const step = steps(found.run).find((s) => s.id === id);
309
+ return step ? ctx.reply(step) : missing(ctx, `No run step found with id '${id}'.`);
310
+ };
311
+ export const assistants = {
312
+ createAssistant,
313
+ modifyAssistant,
314
+ createThread,
315
+ modifyThread,
316
+ createMessage,
317
+ createRun,
318
+ submitToolOuputsToRun: submitToolOutputs,
319
+ listRunSteps,
320
+ getRunStep,
321
+ getRun: async (ctx) => {
322
+ await work(ctx, at(ctx, 'run_id'));
323
+ return coreAnswer(ctx);
324
+ },
325
+ listRuns: async (ctx) => {
326
+ const threadId = at(ctx, 'thread_id');
327
+ for (const r of ctx.rowsRaw(RUN).filter((x) => x.thread_id === threadId))
328
+ await work(ctx, String(r.id));
329
+ return coreAnswer(ctx);
330
+ },
331
+ };
@@ -0,0 +1,2 @@
1
+ import type { Semantics } from '@volter/world-core';
2
+ export declare const audio: Record<string, Semantics>;
@@ -0,0 +1,27 @@
1
+ import { handleSpeech, handleTranscription } from "../openai-twin.js";
2
+ import { recordUsage, send } from "./shared.js";
3
+ /** A transcription or translation is billed by the seconds of audio heard (the usage report's `seconds`). */
4
+ async function recordSeconds(ctx, r) {
5
+ if (r.status === 200 && 'seconds' in r && typeof r.seconds === 'number')
6
+ await recordUsage(ctx, 'audio_transcriptions', String(ctx.params.model), 0, 0, { seconds: Math.ceil(r.seconds) });
7
+ }
8
+ export const audio = {
9
+ createTranscription: async (ctx) => {
10
+ const r = handleTranscription(ctx.params, false);
11
+ await recordSeconds(ctx, r);
12
+ return 'events' in r && r.events ? ctx.sse(r.events) : send(ctx, r);
13
+ },
14
+ createTranslation: async (ctx) => {
15
+ const r = handleTranscription(ctx.params, true);
16
+ await recordSeconds(ctx, r);
17
+ return send(ctx, r);
18
+ },
19
+ // the audio file itself, not JSON
20
+ createSpeech: async (ctx) => {
21
+ const r = handleSpeech(ctx.params);
22
+ // speech is billed by the characters it speaks (the usage report's `characters`)
23
+ if ('audio' in r)
24
+ await recordUsage(ctx, 'audio_speeches', String(ctx.params.model), 0, 0, { characters: String(ctx.params.input ?? '').length });
25
+ return 'audio' in r ? ctx.raw(r.audio, { headers: { 'content-type': r.type } }) : send(ctx, r);
26
+ },
27
+ };
@@ -0,0 +1,4 @@
1
+ import type { Semantics, SemanticsContext } from '@volter/world-core';
2
+ export declare const batches: Record<string, Semantics>;
3
+ /** Every batch OpenAI is still working on, observed (a read observes all the vendor's work before it answers). */
4
+ export declare function observeBatches(ctx: SemanticsContext): Promise<void>;
@@ -0,0 +1,86 @@
1
+ import { coreAnswer, inFlight, progress } from "./progress.js";
2
+ import { epoch, invalid, objectOr } from "./shared.js";
3
+ /** The request lines of a batch's input file. */
4
+ const requestLines = (ctx, row) => String(ctx.row('OpenAIFile', String(row.input_file_id), { withDeleted: true })?._content ?? '').split('\n').filter((l) => l.trim());
5
+ /** What a finished batch carries: its output file and its request counts, every request answered
6
+ * (https://platform.openai.com/docs/api-reference/batch/object, `request_counts`). */
7
+ const finish = (ctx) => (row, end) => {
8
+ if (end !== 'completed')
9
+ return {};
10
+ const total = requestLines(ctx, row).length;
11
+ return { output_file_id: `file-twin-batchout-${String(row.id)}`, request_counts: { total, completed: total, failed: 0 } };
12
+ };
13
+ const create = async (ctx) => {
14
+ const p = ctx.params;
15
+ if (p.input_file_id === undefined || p.input_file_id === '')
16
+ return invalid(ctx, 'you must provide an input_file_id parameter', 'input_file_id');
17
+ if (p.endpoint === undefined || p.endpoint === '')
18
+ return invalid(ctx, 'you must provide an endpoint parameter', 'endpoint');
19
+ if (p.completion_window === undefined)
20
+ return invalid(ctx, 'you must provide a completion_window parameter', 'completion_window');
21
+ const id = ctx.mint('Batch');
22
+ const created = epoch(ctx);
23
+ const fields = {
24
+ object: 'batch',
25
+ endpoint: String(p.endpoint),
26
+ input_file_id: String(p.input_file_id),
27
+ completion_window: String(p.completion_window),
28
+ status: 'validating',
29
+ output_file_id: null,
30
+ error_file_id: null,
31
+ created_at: created,
32
+ // the batch object's timestamps, each null until the batch gets there; it expires at the end of its completion
33
+ // window (https://platform.openai.com/docs/api-reference/batch/object)
34
+ in_progress_at: null,
35
+ expires_at: created + 24 * 3600,
36
+ finalizing_at: null,
37
+ completed_at: null,
38
+ failed_at: null,
39
+ expired_at: null,
40
+ cancelling_at: null,
41
+ cancelled_at: null,
42
+ errors: null,
43
+ request_counts: { total: 0, completed: 0, failed: 0 },
44
+ metadata: objectOr(p.metadata, null),
45
+ };
46
+ return ctx.reply(await ctx.write('Batch', id, fields, 'batch.create'));
47
+ };
48
+ /** Work a batch on; the read that completes it writes its output file, a result for each request line of its input. */
49
+ async function work(ctx, id) {
50
+ const moved = await progress(ctx, 'Batch', id, finish(ctx));
51
+ if (moved?.to !== 'completed')
52
+ return;
53
+ const lines = requestLines(ctx, moved.row);
54
+ const results = lines.map((line, i) => {
55
+ let req = {};
56
+ try {
57
+ req = JSON.parse(line);
58
+ }
59
+ catch { /* a line that is not JSON answers an error for it */ }
60
+ return JSON.stringify({ id: `batch_req_twin_${i + 1}`, custom_id: req.custom_id ?? null, response: { status_code: 200, request_id: `req_twin_${i + 1}`, body: { object: 'chat.completion', model: req.body?.model ?? null, choices: [{ index: 0, message: { role: 'assistant', content: '[twin-stub] deterministic batch result' }, finish_reason: 'stop' }] } }, error: null });
61
+ });
62
+ const content = results.length ? `${results.join('\n')}\n` : '';
63
+ // the output is deleted thirty days after the batch completes (developers.openai.com/api/docs/guides/batch)
64
+ await ctx.write('OpenAIFile', String(moved.row.output_file_id), { object: 'file', bytes: new TextEncoder().encode(content).length, created_at: epoch(ctx), expires_at: Number(moved.row.completed_at ?? epoch(ctx)) + 30 * 24 * 3600, filename: 'batch_output.jsonl', purpose: 'batch_output', status: 'processed', _content: content }, 'file.create');
65
+ }
66
+ // the organization's batches, each one's work observed first
67
+ const listBatches = async (ctx) => {
68
+ for (const row of ctx.rowsRaw('Batch'))
69
+ if (inFlight('Batch', row))
70
+ await work(ctx, String(row.id));
71
+ return coreAnswer(ctx);
72
+ };
73
+ export const batches = {
74
+ createBatch: create,
75
+ retrieveBatch: async (ctx) => {
76
+ await work(ctx, String(ctx.id));
77
+ return coreAnswer(ctx);
78
+ },
79
+ listBatches,
80
+ };
81
+ /** Every batch OpenAI is still working on, observed (a read observes all the vendor's work before it answers). */
82
+ export async function observeBatches(ctx) {
83
+ for (const row of ctx.rowsRaw('Batch'))
84
+ if (inFlight('Batch', row))
85
+ await work(ctx, String(row.id));
86
+ }
@@ -0,0 +1,3 @@
1
+ import type { Semantics } from '@volter/world-core';
2
+ import { type OpenAIScenarioEngine } from '../openai-scenario.js';
3
+ export declare function chatCompletions(engine: OpenAIScenarioEngine | undefined): Record<string, Semantics>;
@@ -0,0 +1,58 @@
1
+ import { serveScenario } from "../openai-scenario.js";
2
+ import { buildChatCompletion, streamChat, validateChat } from "../openai-twin.js";
3
+ import { page, recordUsage, send } from "./shared.js";
4
+ const COMPLETION = 'CreateChatCompletionResponse';
5
+ const create = (engine) => async (ctx) => {
6
+ const validated = validateChat(ctx.params);
7
+ if ('error' in validated)
8
+ return send(ctx, validated.error);
9
+ const args = validated.args;
10
+ // the scenario decides the turn and honors a fault before any completion exists: a status fault
11
+ // is this vendor's own refusal envelope, decided before a stream would start
12
+ const decision = engine ? await serveScenario(engine, { model: args.model, messages: args.messages, tools: args.tools, maxTokens: args.maxTokens }, new URL(ctx.call.request.url).pathname) : undefined;
13
+ if (decision?.kind === 'fault')
14
+ return send(ctx, decision.result);
15
+ const events = [];
16
+ const result = args.stream ? streamChat(args, (e) => { if (e.data)
17
+ events.push({ data: e.data }); }, ctx.occurredAt, decision) : buildChatCompletion(args, ctx.occurredAt, decision);
18
+ // the request's messages are kept for the stored completion's /messages
19
+ // as the messages list shows them (https://platform.openai.com/docs/api-reference/chat/getMessages): the text, the
20
+ // author's name or null, and the parts when the content was sent as parts, else null
21
+ const inputMessages = args.messages.map((m, i) => {
22
+ const parts = Array.isArray(m.content) ? m.content : null;
23
+ const content = parts ? parts.filter((c) => c.type === 'text').map((c) => String(c.text ?? '')).join('') : (m.content ?? null);
24
+ return { id: `${result.id}-msg-${i}`, role: m.role, content, name: m.name ?? null, content_parts: parts };
25
+ });
26
+ // a stored completion keeps its metadata, `{}` when none was sent (https://platform.openai.com/docs/api-reference/chat/object)
27
+ // and reads back each reply message with its tool calls and legacy function call, null when it made none (the
28
+ // reference's getChatCompletion and listChatCompletions examples)
29
+ // A stored completion also keeps the request that made it: its id, and the settings it was sampled with, as sent or
30
+ // their defaults (the reference's getChatCompletion example: request_id, seed, temperature, top_p, presence_penalty,
31
+ // frequency_penalty, input_user, tools, tool_choice, response_format; spec/patches.json adds them to the object)
32
+ const p = ctx.params;
33
+ const settings = {
34
+ request_id: `req_twin_${result.id.replace(/^chatcmpl-twin-/, '')}`,
35
+ seed: typeof p.seed === 'number' ? p.seed : parseInt(result.id.replace(/^chatcmpl-twin-/, ''), 36),
36
+ temperature: p.temperature ?? 1, top_p: p.top_p ?? 1, presence_penalty: p.presence_penalty ?? 0, frequency_penalty: p.frequency_penalty ?? 0,
37
+ input_user: p.user ?? null, tools: p.tools ?? null, tool_choice: p.tool_choice ?? null, response_format: p.response_format ?? null,
38
+ };
39
+ const stored = args.store === true ? { metadata: result.metadata ?? {}, ...settings, choices: result.choices.map((c) => ({ ...c, message: { ...c.message, tool_calls: c.message.tool_calls ?? null, function_call: null } })) } : {};
40
+ const { vendorData } = await ctx.writeDetailed(COMPLETION, result.id, { ...result, ...stored, _stored: args.store === true, _input_messages: inputMessages }, 'chat.completions.create');
41
+ // live use: the head performed the call, and the model's own words are the answer
42
+ const live = vendorData;
43
+ const answer = live && typeof live === 'object' && Array.isArray(live.choices) ? live : result;
44
+ await recordUsage(ctx, 'completions', answer.model, answer.usage?.prompt_tokens ?? result.usage.prompt_tokens, answer.usage?.completion_tokens ?? result.usage.completion_tokens);
45
+ return args.stream ? ctx.sse(events) : ctx.reply(answer);
46
+ };
47
+ // the request's messages, kept when the completion was stored
48
+ const messages = async (ctx) => {
49
+ const id = String(ctx.id);
50
+ const row = ctx.row(COMPLETION, id);
51
+ return row ? page(ctx, row._input_messages ?? []) : ctx.notFound(COMPLETION, id);
52
+ };
53
+ export function chatCompletions(engine) {
54
+ return {
55
+ createChatCompletion: create(engine),
56
+ getChatCompletionMessages: messages,
57
+ };
58
+ }
@@ -0,0 +1,2 @@
1
+ import type { Semantics } from '@volter/world-core';
2
+ export declare const containers: Record<string, Semantics>;