@volter/twin-openai 0.1.2 → 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.
- package/README.md +33 -30
- package/defaults/handlers.json +10 -0
- package/dist/defaults/handlers.json +10 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +29 -0
- package/dist/src/generated/surface.gen.json +1 -0
- package/dist/src/generated/ui.gen.json +1 -0
- package/dist/src/index.d.ts +19 -0
- package/dist/src/index.js +72 -0
- package/dist/src/manifest.d.ts +6 -0
- package/dist/src/manifest.js +323 -0
- package/dist/src/openai-budget.d.ts +53 -0
- package/dist/src/openai-budget.js +147 -0
- package/dist/src/openai-capabilities.d.ts +4 -0
- package/dist/src/openai-capabilities.js +1569 -0
- package/dist/src/openai-conformance.d.ts +13 -0
- package/dist/src/openai-conformance.js +116 -0
- package/dist/src/openai-connector.d.ts +86 -0
- package/dist/src/openai-connector.js +291 -0
- package/dist/src/openai-media.d.ts +43 -0
- package/dist/src/openai-media.js +257 -0
- package/dist/src/openai-models.d.ts +74 -0
- package/dist/src/openai-models.js +148 -0
- package/dist/src/openai-scenario.d.ts +51 -0
- package/dist/src/openai-scenario.js +166 -0
- package/dist/src/openai-server.d.ts +40 -0
- package/dist/src/openai-server.js +126 -0
- package/dist/src/openai-stub.d.ts +82 -0
- package/dist/src/openai-stub.js +256 -0
- package/dist/src/openai-twin.d.ts +182 -0
- package/dist/src/openai-twin.js +1117 -0
- package/dist/src/openai-types.d.ts +194 -0
- package/dist/src/openai-types.js +4 -0
- package/dist/src/openai-webhooks.d.ts +47 -0
- package/dist/src/openai-webhooks.js +99 -0
- package/dist/src/screens/api-keys.d.ts +16 -0
- package/dist/src/screens/api-keys.js +131 -0
- package/dist/src/screens/session.d.ts +22 -0
- package/dist/src/screens/session.js +115 -0
- package/dist/src/semantics/assistants.d.ts +2 -0
- package/dist/src/semantics/assistants.js +331 -0
- package/dist/src/semantics/audio.d.ts +2 -0
- package/dist/src/semantics/audio.js +27 -0
- package/dist/src/semantics/batches.d.ts +4 -0
- package/dist/src/semantics/batches.js +86 -0
- package/dist/src/semantics/chat-completions.d.ts +3 -0
- package/dist/src/semantics/chat-completions.js +58 -0
- package/dist/src/semantics/containers.d.ts +2 -0
- package/dist/src/semantics/containers.js +147 -0
- package/dist/src/semantics/embeddings.d.ts +2 -0
- package/dist/src/semantics/embeddings.js +13 -0
- package/dist/src/semantics/evals.d.ts +2 -0
- package/dist/src/semantics/evals.js +173 -0
- package/dist/src/semantics/files.d.ts +13 -0
- package/dist/src/semantics/files.js +59 -0
- package/dist/src/semantics/fine-tuning.d.ts +4 -0
- package/dist/src/semantics/fine-tuning.js +178 -0
- package/dist/src/semantics/images.d.ts +2 -0
- package/dist/src/semantics/images.js +18 -0
- package/dist/src/semantics/index.d.ts +8 -0
- package/dist/src/semantics/index.js +46 -0
- package/dist/src/semantics/models.d.ts +2 -0
- package/dist/src/semantics/models.js +34 -0
- package/dist/src/semantics/moderations.d.ts +2 -0
- package/dist/src/semantics/moderations.js +12 -0
- package/dist/src/semantics/organization.d.ts +2 -0
- package/dist/src/semantics/organization.js +67 -0
- package/dist/src/semantics/progress.d.ts +22 -0
- package/dist/src/semantics/progress.js +63 -0
- package/dist/src/semantics/responses.d.ts +3 -0
- package/dist/src/semantics/responses.js +153 -0
- package/dist/src/semantics/shared.d.ts +32 -0
- package/dist/src/semantics/shared.js +69 -0
- package/dist/src/semantics/uploads.d.ts +2 -0
- package/dist/src/semantics/uploads.js +84 -0
- package/dist/src/semantics/vector-stores.d.ts +2 -0
- package/dist/src/semantics/vector-stores.js +281 -0
- package/dist/test-fixtures/openai-openapi-operations.SOURCE.md +18 -0
- package/dist/test-fixtures/openai-openapi-operations.json +1849 -0
- package/package.json +21 -10
- package/src/cli.ts +9 -7
- package/src/generated/surface.gen.json +1 -0
- package/src/generated/ui.gen.json +1 -0
- package/src/index.ts +20 -10
- package/src/manifest.ts +343 -0
- package/src/openai-budget.ts +4 -4
- package/src/openai-capabilities.ts +177 -195
- package/src/openai-conformance.ts +1 -1
- package/src/openai-connector.ts +40 -43
- package/src/openai-media.ts +225 -0
- package/src/openai-models.ts +145 -15
- package/src/openai-scenario.ts +46 -10
- package/src/openai-server.ts +65 -108
- package/src/openai-stub.ts +54 -30
- package/src/openai-twin.ts +760 -1665
- package/src/openai-types.ts +24 -6
- package/src/openai-webhooks.ts +2 -1
- package/src/screens/api-keys.tsx +138 -0
- package/src/screens/session.tsx +131 -0
- package/src/semantics/assistants.ts +336 -0
- package/src/semantics/audio.ts +31 -0
- package/src/semantics/batches.ts +88 -0
- package/src/semantics/chat-completions.ts +66 -0
- package/src/semantics/containers.ts +151 -0
- package/src/semantics/embeddings.ts +19 -0
- package/src/semantics/evals.ts +182 -0
- package/src/semantics/files.ts +67 -0
- package/src/semantics/fine-tuning.ts +185 -0
- package/src/semantics/images.ts +23 -0
- package/src/semantics/index.ts +52 -0
- package/src/semantics/models.ts +41 -0
- package/src/semantics/moderations.ts +14 -0
- package/src/semantics/organization.ts +76 -0
- package/src/semantics/progress.ts +72 -0
- package/src/semantics/responses.ts +151 -0
- package/src/semantics/shared.ts +82 -0
- package/src/semantics/uploads.ts +92 -0
- package/src/semantics/vector-stores.ts +279 -0
- package/test-fixtures/openai-openapi-operations.SOURCE.md +4 -5
- package/test-fixtures/openai-openapi-operations.json +224 -1334
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
// Vector store semantics: the store, the files attached to it, file batches, and search. List,
|
|
2
|
+
// retrieve and delete of a store and retrieve of a file batch are the derived core's. An attached file
|
|
3
|
+
// and a file batch are created `in_progress`, as OpenAI creates them; the twin embeds nothing, so a
|
|
4
|
+
// read that first looks at them finds them `completed` (./progress.ts). An attached
|
|
5
|
+
// file is stored as 'vector_store_file' under `<store>::<file>` (one file may sit in several stores)
|
|
6
|
+
// and shown with the file's id by the manifest's view; its operations are handlers, because OpenAI
|
|
7
|
+
// names a missing one by both ids and answers its deletion with the file's.
|
|
8
|
+
import type { Semantics, SemanticsContext } from '@volter/world-core';
|
|
9
|
+
import { coreAnswer, progress, progressAll, readOnly } from './progress.ts';
|
|
10
|
+
import { at, epoch, invalid, missing, objectOr, page, type Row } from './shared.ts';
|
|
11
|
+
|
|
12
|
+
const STORE = 'VectorStoreObject';
|
|
13
|
+
const FILE = 'VectorStoreFileObject';
|
|
14
|
+
const BATCH = 'VectorStoreFileBatchObject';
|
|
15
|
+
|
|
16
|
+
async function attach(ctx: SemanticsContext, storeId: string, fileId: string, batchId?: string, attributes?: unknown): Promise<Row> {
|
|
17
|
+
// a file detached earlier is attached anew; its attributes are what search filters read
|
|
18
|
+
// (https://platform.openai.com/docs/api-reference/vector-stores-files/createFile, `attributes`)
|
|
19
|
+
const fields = { object: 'vector_store.file', vector_store_id: storeId, file_id: fileId, created_at: epoch(ctx), status: 'in_progress', usage_bytes: 0, ...(attributes && typeof attributes === 'object' ? { attributes } : {}), deleted: false, ...(batchId ? { _batch_id: batchId } : {}) };
|
|
20
|
+
return ctx.write(FILE, `${storeId}::${fileId}`, fields, 'vector_store_file.create');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** What an indexed file carries: the bytes it uses in the store, its File's size (the twin chunks and embeds nothing, so
|
|
24
|
+
* no more and no less: the spec's VectorStoreFileObject `usage_bytes`, "The total vector store usage in bytes"). */
|
|
25
|
+
const indexed = (ctx: SemanticsContext) => (row: Row, end: string): Row => (end === 'completed' ? { usage_bytes: Number(ctx.row('OpenAIFile', String(row.file_id), { withDeleted: true })?.bytes ?? 0) } : {});
|
|
26
|
+
|
|
27
|
+
/** A store's size, the bytes its files use (the spec's VectorStoreObject `usage_bytes`: "The total number of bytes used by
|
|
28
|
+
* the files in the vector store."; `bytes`, which the reference's examples answer beside it, is the same size), written
|
|
29
|
+
* when it changes. */
|
|
30
|
+
async function recount(ctx: SemanticsContext, storeId: string): Promise<void> {
|
|
31
|
+
const row = ctx.row(STORE, storeId, { withDeleted: true });
|
|
32
|
+
if (!row || row.deleted) return;
|
|
33
|
+
const used = attached(ctx, storeId).reduce((n, f) => n + Number(f.usage_bytes ?? 0), 0);
|
|
34
|
+
if (Number(row.usage_bytes) !== used || Number(row.bytes) !== used) await ctx.write(STORE, storeId, { usage_bytes: used, bytes: used }, 'vector_store.update');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A store as a read finds it: the vendor's indexing of its files observed first, and its size with them. */
|
|
38
|
+
async function observeStore(ctx: SemanticsContext, storeId: string): Promise<void> {
|
|
39
|
+
for (const b of new Set(attached(ctx, storeId).map((r) => r._batch_id).filter((x) => x !== undefined))) await landCancel(ctx, String(b));
|
|
40
|
+
await progressAll(ctx, FILE, indexed(ctx), (r) => r.vector_store_id === storeId && !cancelPending(ctx, r._batch_id));
|
|
41
|
+
await recount(ctx, storeId);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The store the path names (a deleted store still holds its files), or OpenAI's answer that there is none. */
|
|
45
|
+
function store(ctx: SemanticsContext): { id: string } | { answer: Response } {
|
|
46
|
+
const id = at(ctx, 'vector_store_id');
|
|
47
|
+
return ctx.row(STORE, id, { withDeleted: true }) ? { id } : { answer: ctx.notFound(STORE, id) };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The store's attached files, bookkeeping included (which batch attached each). */
|
|
51
|
+
const attached = (ctx: SemanticsContext, storeId: string): Row[] => ctx.rowsRaw(FILE).filter((r) => r.vector_store_id === storeId);
|
|
52
|
+
|
|
53
|
+
/** A finished file batch counts its files done. */
|
|
54
|
+
const batchDone = (row: Row, end: string): Row => {
|
|
55
|
+
const total = Number((row.file_counts as { total?: unknown } | undefined)?.total ?? 0);
|
|
56
|
+
return end === 'completed' ? { file_counts: { in_progress: 0, completed: total, failed: 0, cancelled: 0, total } } : {};
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** Observe the vendor's work on a file batch and the files it attached. */
|
|
60
|
+
async function workBatch(ctx: SemanticsContext, batchId: string): Promise<void> {
|
|
61
|
+
// a batch being cancelled does not finish: it stops once its cancel lands
|
|
62
|
+
if (await landCancel(ctx, batchId)) return;
|
|
63
|
+
await progress(ctx, BATCH, batchId, batchDone);
|
|
64
|
+
await progressAll(ctx, FILE, indexed(ctx), (r) => r._batch_id === batchId);
|
|
65
|
+
const b = ctx.row(BATCH, batchId);
|
|
66
|
+
if (b) await recount(ctx, String(b.vector_store_id));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const create: Semantics = async (ctx) => {
|
|
70
|
+
const p = ctx.params;
|
|
71
|
+
const id = ctx.mint(STORE);
|
|
72
|
+
const created = epoch(ctx);
|
|
73
|
+
const fileIds = Array.isArray(p.file_ids) ? (p.file_ids as unknown[]) : [];
|
|
74
|
+
const fields = {
|
|
75
|
+
object: 'vector_store',
|
|
76
|
+
created_at: created,
|
|
77
|
+
name: typeof p.name === 'string' ? p.name : null,
|
|
78
|
+
// what it is for, when the request says, and its size, as the reference's create example answers them
|
|
79
|
+
// (spec/patches.json adds them to the store object)
|
|
80
|
+
description: typeof p.description === 'string' ? p.description : null,
|
|
81
|
+
bytes: 0,
|
|
82
|
+
usage_bytes: 0,
|
|
83
|
+
status: 'completed',
|
|
84
|
+
file_counts: { in_progress: 0, completed: fileIds.length, failed: 0, cancelled: 0, total: fileIds.length },
|
|
85
|
+
metadata: objectOr(p.metadata, {}),
|
|
86
|
+
last_active_at: created,
|
|
87
|
+
};
|
|
88
|
+
const body = await ctx.write(STORE, id, fields, 'vector_store.create');
|
|
89
|
+
for (const fid of fileIds) await attach(ctx, id, String(fid));
|
|
90
|
+
return ctx.reply(body);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const createFile: Semantics = async (ctx) => {
|
|
94
|
+
const found = store(ctx);
|
|
95
|
+
if ('answer' in found) return found.answer;
|
|
96
|
+
if (ctx.params.file_id === undefined || ctx.params.file_id === '') return invalid(ctx, 'you must provide a file_id parameter', 'file_id');
|
|
97
|
+
return ctx.reply(await attach(ctx, found.id, String(ctx.params.file_id), undefined, ctx.params.attributes));
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const listFiles: Semantics = async (ctx) => {
|
|
101
|
+
const found = store(ctx);
|
|
102
|
+
if ('answer' in found) return found.answer;
|
|
103
|
+
for (const b of new Set(attached(ctx, found.id).map((r) => r._batch_id).filter((x) => x !== undefined))) await landCancel(ctx, String(b));
|
|
104
|
+
await progressAll(ctx, FILE, indexed(ctx), (r) => r.vector_store_id === found.id && !cancelPending(ctx, r._batch_id));
|
|
105
|
+
await recount(ctx, found.id);
|
|
106
|
+
return page(ctx, attached(ctx, found.id).map((r) => ctx.get(FILE, String(r.id))!));
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/** The attached file the path names, or OpenAI's answer naming both ids. */
|
|
110
|
+
function attachedFile(ctx: SemanticsContext, storeId: string): { key: string; file: Row } | { answer: Response } {
|
|
111
|
+
const fileId = at(ctx, 'file_id');
|
|
112
|
+
const key = `${storeId}::${fileId}`;
|
|
113
|
+
const file = ctx.get(FILE, key);
|
|
114
|
+
return file ? { key, file } : { answer: missing(ctx, `No file found with id '${fileId}' in vector store '${storeId}'.`) };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const getFile: Semantics = async (ctx) => {
|
|
118
|
+
const found = store(ctx);
|
|
119
|
+
if ('answer' in found) return found.answer;
|
|
120
|
+
const key = `${found.id}::${at(ctx, 'file_id')}`;
|
|
121
|
+
const batchOf = ctx.row(FILE, key)?._batch_id;
|
|
122
|
+
if (batchOf !== undefined) await landCancel(ctx, String(batchOf));
|
|
123
|
+
if (!cancelPending(ctx, batchOf)) await progress(ctx, FILE, key, indexed(ctx));
|
|
124
|
+
await recount(ctx, found.id);
|
|
125
|
+
const hit = attachedFile(ctx, found.id);
|
|
126
|
+
return 'answer' in hit ? hit.answer : ctx.reply(hit.file);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// detaching a file leaves the File itself in place
|
|
130
|
+
const deleteFile: Semantics = async (ctx) => {
|
|
131
|
+
const found = store(ctx);
|
|
132
|
+
if ('answer' in found) return found.answer;
|
|
133
|
+
const hit = attachedFile(ctx, found.id);
|
|
134
|
+
if ('answer' in hit) return hit.answer;
|
|
135
|
+
await ctx.write(FILE, hit.key, { deleted: true }, 'vector_store_file.delete');
|
|
136
|
+
await recount(ctx, found.id);
|
|
137
|
+
return ctx.reply({ id: hit.file.id, object: 'vector_store.file.deleted', deleted: true });
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// A file batch attaches each file, then records the batch, in progress until a read looks.
|
|
141
|
+
const createBatch: Semantics = async (ctx) => {
|
|
142
|
+
const found = store(ctx);
|
|
143
|
+
if ('answer' in found) return found.answer;
|
|
144
|
+
// the files as ids (`file_ids`) or each with its own attributes (`files`), one or the other
|
|
145
|
+
// (https://platform.openai.com/docs/api-reference/vector-stores-file-batches/createBatch)
|
|
146
|
+
const ids = ctx.params.file_ids;
|
|
147
|
+
const files = ctx.params.files;
|
|
148
|
+
const entries: Row[] | undefined = Array.isArray(files) ? (files as Row[]) : Array.isArray(ids) ? ids.map((file_id) => ({ file_id })) : undefined;
|
|
149
|
+
if (!entries || entries.length === 0 || (Array.isArray(files) && Array.isArray(ids))) return invalid(ctx, 'you must provide a non-empty file_ids or files array, not both', Array.isArray(files) ? 'files' : 'file_ids');
|
|
150
|
+
const fileIds = entries.map((e) => String(e.file_id));
|
|
151
|
+
const batchId = ctx.mint(BATCH);
|
|
152
|
+
for (const e of entries) await attach(ctx, found.id, String(e.file_id), batchId, e.attributes);
|
|
153
|
+
const fields = {
|
|
154
|
+
object: 'vector_store.files_batch',
|
|
155
|
+
vector_store_id: found.id,
|
|
156
|
+
created_at: epoch(ctx),
|
|
157
|
+
status: 'in_progress',
|
|
158
|
+
file_counts: { in_progress: fileIds.length, completed: 0, failed: 0, cancelled: 0, total: fileIds.length },
|
|
159
|
+
_file_ids: fileIds,
|
|
160
|
+
};
|
|
161
|
+
return ctx.reply(await ctx.write(BATCH, batchId, fields, 'vector_store_file_batch.create'));
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const listBatchFiles: Semantics = async (ctx) => {
|
|
165
|
+
const found = store(ctx);
|
|
166
|
+
if ('answer' in found) return found.answer;
|
|
167
|
+
const batchId = at(ctx, 'batch_id');
|
|
168
|
+
if (!ctx.row(BATCH, batchId)) return ctx.notFound(BATCH, batchId);
|
|
169
|
+
await workBatch(ctx, batchId);
|
|
170
|
+
return page(ctx, attached(ctx, found.id).filter((r) => r._batch_id === batchId).map((r) => ctx.get(FILE, String(r.id))!));
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
/** A deterministic pseudo-score for (query, file): the twin embeds nothing, so it cannot rank by meaning. */
|
|
174
|
+
function score(query: string, fileId: string): number {
|
|
175
|
+
let h = 0x811c9dc5;
|
|
176
|
+
const s = query + '|' + fileId;
|
|
177
|
+
for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 0x01000193); }
|
|
178
|
+
return (h >>> 0) / 0xffffffff;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** A comparison filter's operators (the spec's ComparisonFilter `type`). */
|
|
182
|
+
const COMPARE: Record<string, (v: unknown, want: unknown) => boolean> = {
|
|
183
|
+
eq: (v, w) => v === w, ne: (v, w) => v !== w,
|
|
184
|
+
gt: (v, w) => v !== undefined && (v as number) > (w as number), gte: (v, w) => v !== undefined && (v as number) >= (w as number),
|
|
185
|
+
lt: (v, w) => v !== undefined && (v as number) < (w as number), lte: (v, w) => v !== undefined && (v as number) <= (w as number),
|
|
186
|
+
in: (v, w) => Array.isArray(w) && w.includes(v), nin: (v, w) => Array.isArray(w) && !w.includes(v),
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/** Whether a file's attributes pass a search filter: a comparison of one attribute to a value, or an `and`/`or` of
|
|
190
|
+
* filters (the spec's ComparisonFilter and CompoundFilter); no filter passes every file. */
|
|
191
|
+
function passes(filter: unknown, attributes: Row): boolean {
|
|
192
|
+
if (!filter || typeof filter !== 'object') return true;
|
|
193
|
+
const f = filter as { type?: unknown; key?: unknown; value?: unknown; filters?: unknown };
|
|
194
|
+
const all = Array.isArray(f.filters) ? f.filters : [];
|
|
195
|
+
return f.type === 'and' ? all.every((x) => passes(x, attributes)) : f.type === 'or' ? all.some((x) => passes(x, attributes)) : (COMPARE[String(f.type ?? 'eq')]?.(attributes[String(f.key)], f.value) ?? false);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// the store's files that pass the filter, ranked by the pseudo-score, in OpenAI's search-results page, each named by
|
|
199
|
+
// its File's filename and carrying its attributes (https://platform.openai.com/docs/api-reference/vector-stores/search)
|
|
200
|
+
const search: Semantics = async (ctx) => {
|
|
201
|
+
const found = store(ctx);
|
|
202
|
+
if ('answer' in found) return found.answer;
|
|
203
|
+
const p = ctx.params;
|
|
204
|
+
if (p.query === undefined || p.query === '') return invalid(ctx, 'you must provide a query parameter', 'query');
|
|
205
|
+
const query = typeof p.query === 'string' ? p.query : JSON.stringify(p.query);
|
|
206
|
+
const maxResults = Number.isInteger(Number(p.max_num_results)) ? Number(p.max_num_results) : 10;
|
|
207
|
+
const data = attached(ctx, found.id)
|
|
208
|
+
.filter((f) => passes(p.filters, (f.attributes ?? {}) as Row))
|
|
209
|
+
.map((f) => ({ file_id: String(f.file_id ?? ''), attributes: (f.attributes ?? {}) as Row, score: score(query, String(f.file_id ?? '')) }))
|
|
210
|
+
.sort((a, b) => b.score - a.score)
|
|
211
|
+
.slice(0, Math.max(1, maxResults))
|
|
212
|
+
.map((r) => ({ file_id: r.file_id, filename: String(ctx.row('OpenAIFile', r.file_id, { withDeleted: true })?.filename ?? r.file_id), score: r.score, attributes: r.attributes, content: [{ type: 'text', text: `[twin-stub] deterministic pseudo-match for "${query}" in ${r.file_id} (no real semantic search)` }] }));
|
|
213
|
+
return ctx.reply({ object: 'vector_store.search_results.page', search_query: query, data, has_more: false, next_page: null });
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// Cancelling a batch "attempts to cancel the processing of files in this batch as soon as possible"
|
|
217
|
+
// (https://platform.openai.com/docs/api-reference/vector-stores-file-batches/cancelBatch): the cancel answers the batch as
|
|
218
|
+
// it stands, still `in_progress`, and the batch stops once the World clock passes the work already in flight, each of its
|
|
219
|
+
// files still in progress cancelled; what it finished stays finished. Extrapolation, the twin's own timing: the work in
|
|
220
|
+
// flight takes a second.
|
|
221
|
+
const CANCEL_SECONDS = 1;
|
|
222
|
+
|
|
223
|
+
/** Whether a batch has a cancel asked for that has not landed yet. */
|
|
224
|
+
const cancelPending = (ctx: SemanticsContext, batchId: unknown): boolean => batchId !== undefined && ctx.row(BATCH, String(batchId))?._cancel_asked_at !== undefined && ctx.row(BATCH, String(batchId))?.status === 'in_progress';
|
|
225
|
+
|
|
226
|
+
/** A batch whose cancel is due stops: its files still in progress cancelled, its counts with them. */
|
|
227
|
+
async function landCancel(ctx: SemanticsContext, batchId: string): Promise<boolean> {
|
|
228
|
+
const batch = ctx.row(BATCH, batchId);
|
|
229
|
+
if (!batch || batch.status !== 'in_progress' || batch._cancel_asked_at === undefined || batch._cancel_asked_at === null) return false;
|
|
230
|
+
if (epoch(ctx) < Number(batch._cancel_asked_at) + CANCEL_SECONDS) return true;
|
|
231
|
+
// the vendor's move, not the caller's: the cancel lands when a read looks
|
|
232
|
+
if (ctx.legal(BATCH, 'status', ctx.call.operation.id, batch.status, 'cancelled', batchId, 'vendor')) return true;
|
|
233
|
+
const stopped = attached(ctx, String(batch.vector_store_id)).filter((r) => r._batch_id === batchId && r.status === 'in_progress');
|
|
234
|
+
for (const f of stopped) {
|
|
235
|
+
ctx.legal(FILE, 'status', ctx.call.operation.id, f.status, 'cancelled', String(f.id), 'vendor');
|
|
236
|
+
await ctx.write(FILE, String(f.id), { status: 'cancelled' }, 'vector_store_file.cancel');
|
|
237
|
+
}
|
|
238
|
+
const total = Number((batch.file_counts as { total?: unknown } | undefined)?.total ?? 0);
|
|
239
|
+
const file_counts = { in_progress: 0, completed: total - stopped.length, failed: 0, cancelled: stopped.length, total };
|
|
240
|
+
await ctx.write(BATCH, batchId, { status: 'cancelled', file_counts }, 'vector_store_file_batch.cancel');
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const cancelBatch: Semantics = async (ctx) => {
|
|
245
|
+
const found = store(ctx);
|
|
246
|
+
if ('answer' in found) return found.answer;
|
|
247
|
+
// a cancel is not a read: it finds the batch where the vendor's work has got to without looking
|
|
248
|
+
const batchId = at(ctx, 'batch_id');
|
|
249
|
+
const batch = ctx.row(BATCH, batchId);
|
|
250
|
+
if (!batch || batch.vector_store_id !== found.id) return ctx.notFound(BATCH, batchId);
|
|
251
|
+
const refusal = ctx.legal(BATCH, 'status', 'cancelVectorStoreFileBatch', batch.status, 'cancelled');
|
|
252
|
+
if (refusal) return ctx.refuse(refusal);
|
|
253
|
+
if (batch._cancel_asked_at !== undefined && batch._cancel_asked_at !== null) return ctx.reply(ctx.get(BATCH, batchId)!);
|
|
254
|
+
return ctx.reply(await ctx.write(BATCH, batchId, { _cancel_asked_at: epoch(ctx) }, 'vector_store_file_batch.update'));
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
export const vectorStores: Record<string, Semantics> = {
|
|
258
|
+
getVectorStore: async (ctx) => {
|
|
259
|
+
if (!readOnly(ctx)) await observeStore(ctx, at(ctx, 'vector_store_id'));
|
|
260
|
+
return coreAnswer(ctx);
|
|
261
|
+
},
|
|
262
|
+
listVectorStores: async (ctx) => {
|
|
263
|
+
if (!readOnly(ctx)) for (const r of ctx.rowsRaw(STORE)) await observeStore(ctx, String(r.id));
|
|
264
|
+
return coreAnswer(ctx);
|
|
265
|
+
},
|
|
266
|
+
cancelVectorStoreFileBatch: cancelBatch,
|
|
267
|
+
createVectorStore: create,
|
|
268
|
+
createVectorStoreFile: createFile,
|
|
269
|
+
listVectorStoreFiles: listFiles,
|
|
270
|
+
getVectorStoreFile: getFile,
|
|
271
|
+
getVectorStoreFileBatch: async (ctx) => {
|
|
272
|
+
await workBatch(ctx, at(ctx, 'batch_id'));
|
|
273
|
+
return coreAnswer(ctx);
|
|
274
|
+
},
|
|
275
|
+
deleteVectorStoreFile: deleteFile,
|
|
276
|
+
createVectorStoreFileBatch: createBatch,
|
|
277
|
+
listFilesInVectorStoreBatch: listBatchFiles,
|
|
278
|
+
searchVectorStore: search,
|
|
279
|
+
};
|
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
# openai-openapi-operations.json — provenance
|
|
2
2
|
|
|
3
3
|
The operation-inventory denominator for `../openai-spec-census.json`: a scope-filtered subset of
|
|
4
|
-
the vendor's published spec, enumerated by `deriveFromOpenAPI` (`packages/
|
|
4
|
+
the vendor's published spec, enumerated by `deriveFromOpenAPI` (`packages/world-tooling/src/derive.ts`).
|
|
5
5
|
|
|
6
6
|
- **Upstream:** https://github.com/openai/openai-openapi — `openapi.yaml`, fetched 2026-09-02 from
|
|
7
7
|
`https://raw.githubusercontent.com/openai/openai-openapi/master/openapi.yaml` at commit
|
|
8
8
|
`18a43ed13461a01fce5b9cc93c86843c550734a3` (2026-09-01), spec `info.version` 2.3.0.
|
|
9
9
|
- **How it was built:** `bun scripts/spec-census-bootstrap.ts --vendor openai --from-file <openapi.yaml> --scope-paths /assistants,/audio,/batches,/chat/,/containers,/embeddings,/evals,/files,/fine_tuning,/images,/models,/moderations,/organization,/responses,/threads,/uploads,/vector_stores --apply`
|
|
10
|
-
over a copy of the upstream file fetched 2026-09-02; operation metadata only (operationId, summary, tags,
|
|
11
|
-
first response), schemas dropped. The gate never fetches — refreshing the fixture is
|
|
10
|
+
over a copy of the upstream file fetched 2026-09-02; operation metadata only (operationId, summary, tags), schemas and response codes dropped. The gate never fetches — refreshing the fixture is
|
|
12
11
|
`bun scripts/spec-refresh.ts --vendor openai --from-file <newer spec> --apply` over a newly fetched copy.
|
|
13
12
|
- **Scope (222 of 281 upstream operations):** every operation whose path starts with one of
|
|
14
13
|
`/assistants`, `/audio`, `/batches`, `/chat/`, `/containers`, `/embeddings`, `/evals`, `/files`, `/fine_tuning`, `/images`, `/models`, `/moderations`, `/organization`, `/responses`, `/threads`, `/uploads`, `/vector_stores`.
|
|
15
14
|
- **Mapping:** 128 operations mapped to capability ids by segment match at birth, then reviewed by
|
|
16
15
|
hand against the router (`routeOpenAI` in `../src/openai-twin.ts`) and each capability's `verify()`:
|
|
17
|
-
60 entries were re-ruled, landing at **97 capability / 0
|
|
18
|
-
baseline). Every later ruling (
|
|
16
|
+
60 entries were re-ruled, landing at **97 capability / 0 $ruled / 125 unmapped** (the ratchet
|
|
17
|
+
baseline). Every later ruling (a `$ruled` marker, a remap) is an edit to the census, never to
|
|
19
18
|
this fixture.
|