@volter/twin-openai 0.1.2 → 2.0.1

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 +3 -2
  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,147 @@
1
+ import { coreAnswer, readOnly } from "./progress.js";
2
+ import { at, epoch, invalid, objectOr, recordUsage } from "./shared.js";
3
+ const CONTAINER = 'ContainerResource';
4
+ const FILE = 'ContainerFileResource';
5
+ /** Whether a running container has sat idle past its expiry window at this instant. */
6
+ function idle(row, now) {
7
+ const after = (row.expires_after ?? {});
8
+ const minutes = Number(after.minutes ?? 20);
9
+ const anchor = Number((after.anchor === 'created_at' ? row.created_at : row.last_active_at) ?? row.created_at ?? 0);
10
+ return row.status === 'running' && Number.isFinite(minutes) && now > anchor + minutes * 60;
11
+ }
12
+ /** Observe a container's expiry (the time move), deciding and writing at once. */
13
+ async function observe(ctx, id) {
14
+ if (readOnly(ctx))
15
+ return;
16
+ const now = epoch(ctx);
17
+ await ctx.atomically((rows) => {
18
+ const current = rows(CONTAINER).find((r) => r.id === id);
19
+ if (!current || !idle(current, now))
20
+ return { value: undefined };
21
+ if (ctx.legal(CONTAINER, 'status', ctx.call.operation.id, current.status, 'expired', id, 'time'))
22
+ return { value: undefined };
23
+ return { value: undefined, write: { resource: CONTAINER, id, fields: { status: 'expired' }, operation: 'container.update' } };
24
+ });
25
+ }
26
+ /** The container a file operation names, observed and allowed to be used, or OpenAI's answer. */
27
+ async function usable(ctx) {
28
+ const id = at(ctx, 'container_id');
29
+ await observe(ctx, id);
30
+ const row = ctx.get(CONTAINER, id);
31
+ if (!row)
32
+ return { answer: ctx.notFound(CONTAINER, id) };
33
+ const refusal = ctx.legal(CONTAINER, 'status', ctx.call.operation.id, row.status, undefined, id);
34
+ return refusal ? { answer: ctx.refuse(refusal) } : { id };
35
+ }
36
+ /** Using a container (a file added or removed) is activity: it restarts the idle window. */
37
+ const touch = (ctx, id) => ctx.write(CONTAINER, id, { last_active_at: epoch(ctx) }, 'container.update');
38
+ const create = async (ctx) => {
39
+ const p = ctx.params;
40
+ if (p.name === undefined || p.name === '')
41
+ return invalid(ctx, 'you must provide a name parameter', 'name');
42
+ const created = epoch(ctx);
43
+ const fields = {
44
+ object: 'container',
45
+ name: String(p.name),
46
+ created_at: created,
47
+ status: 'running',
48
+ last_active_at: created,
49
+ expires_after: objectOr(p.expires_after, { anchor: 'last_active_at', minutes: 20 }),
50
+ // its memory, "Defaults to \"1g\"", and the network policy it was given (the spec's CreateContainerBody;
51
+ // https://platform.openai.com/docs/api-reference/containers/createContainers)
52
+ memory_limit: typeof p.memory_limit === 'string' && p.memory_limit ? p.memory_limit : '1g',
53
+ ...(p.network_policy && typeof p.network_policy === 'object' ? { network_policy: p.network_policy } : {}),
54
+ };
55
+ const written = await ctx.write(CONTAINER, ctx.mint(CONTAINER), fields, 'container.create');
56
+ // a container is a code interpreter session, billed as one (the usage report's `num_sessions`)
57
+ await recordUsage(ctx, 'code_interpreter_sessions', 'code-interpreter', 0, 0, { num_sessions: 1 });
58
+ return ctx.reply(written);
59
+ };
60
+ // A file comes from a stored File (a JSON `file_id`) or a multipart upload (the part `file`), the two
61
+ // forms OpenAI takes (https://platform.openai.com/docs/api-reference/container-files/createContainerFile:
62
+ // "either a multipart/form-data request with the raw file content, or a JSON request with a file ID");
63
+ // its id is the container's next.
64
+ const createFile = async (ctx) => {
65
+ const found = await usable(ctx);
66
+ if ('answer' in found)
67
+ return found.answer;
68
+ const containerId = found.id;
69
+ const p = ctx.params;
70
+ const seq = ctx.rowsRaw(FILE, { withDeleted: true }).filter((r) => r.container_id === containerId).length + 1;
71
+ const id = `cfile-twin-${containerId}-${seq}`;
72
+ let content = '';
73
+ let source = 'user';
74
+ let name = '';
75
+ if (typeof p.file_id === 'string' && p.file_id) {
76
+ const file = ctx.row('OpenAIFile', p.file_id);
77
+ if (!file)
78
+ return ctx.notFound('OpenAIFile', p.file_id);
79
+ content = String(file._content ?? '');
80
+ source = 'file_id';
81
+ }
82
+ else if (p.file && typeof p.file === 'object') {
83
+ const upload = p.file;
84
+ content = String(upload.content ?? '');
85
+ name = typeof upload.name === 'string' && upload.name ? upload.name : '';
86
+ }
87
+ const fields = {
88
+ object: 'container.file',
89
+ container_id: containerId,
90
+ created_at: epoch(ctx),
91
+ bytes: content.length,
92
+ path: name ? `/mnt/data/${name}` : `/mnt/data/${id}`,
93
+ source,
94
+ _content: content,
95
+ };
96
+ const written = await ctx.write(FILE, id, fields, 'container_file.create');
97
+ await touch(ctx, containerId);
98
+ return ctx.reply(written);
99
+ };
100
+ // the file's bytes as kept
101
+ const content = async (ctx) => {
102
+ const found = await usable(ctx);
103
+ if ('answer' in found)
104
+ return found.answer;
105
+ const id = at(ctx, 'file_id');
106
+ const file = ctx.row(FILE, id);
107
+ if (!file || file.container_id !== at(ctx, 'container_id'))
108
+ return ctx.notFound(FILE, id);
109
+ return ctx.raw(String(file._content ?? ''), { headers: { 'content-type': 'application/octet-stream' } });
110
+ };
111
+ export const containers = {
112
+ CreateContainer: create,
113
+ CreateContainerFile: createFile,
114
+ RetrieveContainerFileContent: content,
115
+ // a container's reads observe its expiry first
116
+ RetrieveContainer: async (ctx) => {
117
+ await observe(ctx, at(ctx, 'container_id'));
118
+ return coreAnswer(ctx);
119
+ },
120
+ ListContainers: async (ctx) => {
121
+ for (const r of ctx.rowsRaw(CONTAINER))
122
+ await observe(ctx, String(r.id));
123
+ return coreAnswer(ctx);
124
+ },
125
+ // its files' reads, only while it can be used
126
+ ListContainerFiles: async (ctx) => {
127
+ const found = await usable(ctx);
128
+ return 'answer' in found ? found.answer : coreAnswer(ctx);
129
+ },
130
+ RetrieveContainerFile: async (ctx) => {
131
+ const found = await usable(ctx);
132
+ return 'answer' in found ? found.answer : coreAnswer(ctx);
133
+ },
134
+ // removing a file is activity too
135
+ DeleteContainerFile: async (ctx) => {
136
+ const found = await usable(ctx);
137
+ if ('answer' in found)
138
+ return found.answer;
139
+ const id = at(ctx, 'file_id');
140
+ const file = ctx.get(FILE, id);
141
+ if (!file || file.container_id !== found.id)
142
+ return ctx.notFound(FILE, id);
143
+ await ctx.write(FILE, id, { deleted: true }, 'container_file.delete');
144
+ await touch(ctx, found.id);
145
+ return ctx.reply({ id, object: 'container.file.deleted', deleted: true });
146
+ },
147
+ };
@@ -0,0 +1,2 @@
1
+ import type { Semantics } from '@volter/world-core';
2
+ export declare const embeddings: Record<string, Semantics>;
@@ -0,0 +1,13 @@
1
+ import { handleEmbeddings } from "../openai-twin.js";
2
+ import { recordUsage, send } from "./shared.js";
3
+ const create = async (ctx) => {
4
+ const answer = handleEmbeddings(ctx.params);
5
+ if (answer.status === 200) {
6
+ const body = answer.body;
7
+ await recordUsage(ctx, 'embeddings', body.model, body.usage.prompt_tokens, 0);
8
+ }
9
+ return send(ctx, answer);
10
+ };
11
+ export const embeddings = {
12
+ createEmbedding: create,
13
+ };
@@ -0,0 +1,2 @@
1
+ import type { Semantics } from '@volter/world-core';
2
+ export declare const evals: Record<string, Semantics>;
@@ -0,0 +1,173 @@
1
+ import { coreAnswer, progress, progressAll } from "./progress.js";
2
+ import { at, epoch, invalid, objectOr, page } from "./shared.js";
3
+ const EVAL = 'Eval';
4
+ const RUN = 'EvalRun';
5
+ /** The items a run grades: those its data source holds inline (`file_content`), else one the twin stands in for (it
6
+ * fetches no dataset and reads no stored completions). */
7
+ function items(run) {
8
+ const source = (run.data_source ?? {}).source;
9
+ const inline = source?.type === 'file_content' && Array.isArray(source.content) ? source.content.map((c) => (c?.item ?? {})) : [];
10
+ return inline.length ? inline : [{ input: '[twin-stub] datasource item' }];
11
+ }
12
+ /** The names the eval's testing criteria are graded under: each criterion's id. */
13
+ const criteria = (ctx, run) => {
14
+ const list = (ctx.get(EVAL, String(run.eval_id))?.testing_criteria ?? []);
15
+ return list.length ? list.map((c) => String(c.id)) : ['twin-stub-grader'];
16
+ };
17
+ /** What a graded run carries: each of its items passed by the stub grader, under each criterion. */
18
+ const graded = (ctx) => (row, end) => {
19
+ if (end !== 'completed')
20
+ return {};
21
+ const n = items(row).length;
22
+ return {
23
+ result_counts: { total: n, errored: 0, failed: 0, passed: n },
24
+ per_model_usage: row.model == null ? [] : [{ model_name: String(row.model), invocation_count: n, prompt_tokens: 0, completion_tokens: 0, total_tokens: 0, cached_tokens: 0 }],
25
+ per_testing_criteria_results: criteria(ctx, row).map((testing_criteria) => ({ testing_criteria, passed: n, failed: 0 })),
26
+ };
27
+ };
28
+ /** A chat message as an eval answers it: a typed message whose text content is an `input_text` part (the reference's
29
+ * createEval and createEvalRun examples answer the messages they were sent so). */
30
+ function message(m) {
31
+ const x = m;
32
+ if (!x || typeof x !== 'object' || x.type !== undefined && x.type !== 'message')
33
+ return m;
34
+ return { type: 'message', ...x, content: typeof x.content === 'string' ? { type: 'input_text', text: x.content } : x.content };
35
+ }
36
+ /** The schema of the items (and samples) an eval reads: a stored-completions eval's item and sample objects, a custom
37
+ * eval's item schema as sent (the reference's createEval and getEval examples;
38
+ * https://platform.openai.com/docs/api-reference/evals/object, `data_source_config.schema`). */
39
+ function dataSourceConfig(config) {
40
+ if (config.type === 'stored_completions')
41
+ return { type: 'stored_completions', metadata: config.metadata ?? {}, schema: { type: 'object', properties: { item: { type: 'object' }, sample: { type: 'object' } }, required: ['item', 'sample'] } };
42
+ // a custom eval's item schema (and the sample's, when asked for); any other source's configuration as sent
43
+ const sample = config.include_sample_schema === true;
44
+ return config.type === 'custom' && config.item_schema
45
+ ? { type: 'custom', schema: { type: 'object', properties: { item: config.item_schema, ...(sample ? { sample: { type: 'object' } } : {}) }, required: sample ? ['item', 'sample'] : ['item'] } }
46
+ : config;
47
+ }
48
+ const createEval = async (ctx) => {
49
+ const p = ctx.params;
50
+ if (p.data_source_config === undefined)
51
+ return invalid(ctx, 'you must provide a data_source_config parameter', 'data_source_config');
52
+ if (p.testing_criteria === undefined || !Array.isArray(p.testing_criteria))
53
+ return invalid(ctx, 'you must provide a testing_criteria parameter (array)', 'testing_criteria');
54
+ const id = ctx.mint(EVAL);
55
+ // each criterion is named and given an id of its own (its name and the eval's), its model's messages typed
56
+ // a label model grader samples as its model does unless told: `sampling_params: null` (the reference's listEvals
57
+ // example; spec/patches.json)
58
+ const testing = p.testing_criteria.map((c, i) => ({ ...c, id: `${String(c.name ?? c.type)}-${id}-${i + 1}`, ...(Array.isArray(c.input) ? { input: c.input.map(message) } : {}), ...(c.type === 'label_model' ? { sampling_params: c.sampling_params ?? null } : {}) }));
59
+ const fields = {
60
+ object: 'eval',
61
+ name: typeof p.name === 'string' ? p.name : `eval-${id}`,
62
+ created_at: epoch(ctx),
63
+ data_source_config: dataSourceConfig((p.data_source_config ?? {})),
64
+ testing_criteria: testing,
65
+ metadata: objectOr(p.metadata, {}),
66
+ };
67
+ return ctx.reply(await ctx.write(EVAL, id, fields, 'eval.create'));
68
+ };
69
+ // the twin grades a single datasource item (it fetches no dataset): one item, passed
70
+ const createRun = async (ctx) => {
71
+ const evalId = at(ctx, 'eval_id');
72
+ if (!ctx.get(EVAL, evalId))
73
+ return ctx.notFound(EVAL, evalId);
74
+ const p = ctx.params;
75
+ if (p.data_source === undefined)
76
+ return invalid(ctx, 'you must provide a data_source parameter', 'data_source');
77
+ const id = ctx.mint(RUN);
78
+ const source = p.data_source;
79
+ // a data source that names no model samples none: the run's `model` is "The model that is evaluated, if applicable"
80
+ // (EvalRun), and the spec gives the sources no default; with none, nothing is sampled and nothing billed (the twin's
81
+ // reading of "if applicable")
82
+ const model = typeof source?.model === 'string' ? source.model : null;
83
+ const template = source?.input_messages?.template;
84
+ const dataSource = Array.isArray(template) ? { ...source, input_messages: { ...source.input_messages, template: template.map(message) } } : source;
85
+ const fields = {
86
+ object: 'eval.run',
87
+ eval_id: evalId,
88
+ name: typeof p.name === 'string' ? p.name : `run-${id}`,
89
+ created_at: epoch(ctx),
90
+ status: 'queued',
91
+ model,
92
+ data_source: dataSource,
93
+ // nothing graded yet: no usage and no criterion's results (the reference's createEvalRun example)
94
+ result_counts: { total: 0, errored: 0, failed: 0, passed: 0 },
95
+ per_model_usage: null,
96
+ per_testing_criteria_results: null,
97
+ report_url: `https://twin.invalid/evals/${evalId}/runs/${id}`,
98
+ metadata: objectOr(p.metadata, {}),
99
+ error: null,
100
+ };
101
+ return ctx.reply(await ctx.write(RUN, id, fields, 'eval_run.create'));
102
+ };
103
+ /** The messages the model was sent for an item: the run's template, each `{{item.x}}` filled from the item. */
104
+ function rendered(run, item) {
105
+ const template = ((run.data_source ?? {}).input_messages ?? {}).template;
106
+ const fill = (t) => t.replace(/\{\{\s*item\.([A-Za-z0-9_]+)\s*\}\}/g, (_m, k) => String(item[k] ?? ''));
107
+ return (Array.isArray(template) ? template : []).map((m) => {
108
+ const x = m;
109
+ const text = typeof x.content === 'string' ? x.content : String(x.content?.text ?? '');
110
+ return { role: x.role, content: fill(text), tool_call_id: null, tool_calls: null, function_call: null };
111
+ });
112
+ }
113
+ /** A completed run's output: an item for each datasource item it graded, passed by the stub grader under each
114
+ * criterion, with the sample the model answered (https://platform.openai.com/docs/api-reference/evals/run-output-item-object);
115
+ * others have none yet. */
116
+ function outputItems(ctx, run) {
117
+ if (run.status !== 'completed')
118
+ return [];
119
+ const sampling = ((run.data_source ?? {}).sampling_params ?? {});
120
+ const names = criteria(ctx, run);
121
+ return items(run).map((item, i) => ({
122
+ id: `evalitem-${run.id}-${i + 1}`,
123
+ object: 'eval.run.output_item',
124
+ created_at: Number(run.created_at ?? 0),
125
+ run_id: String(run.id),
126
+ eval_id: String(run.eval_id),
127
+ status: 'pass',
128
+ datasource_item_id: i,
129
+ datasource_item: item,
130
+ results: names.map((name) => ({ name, sample: null, passed: true, score: 1.0 })),
131
+ sample: run.model == null ? null : {
132
+ input: rendered(run, item), output: [{ role: 'assistant', content: '[twin-stub] eval sample output (no real model run)', tool_call_id: null, tool_calls: null, function_call: null }],
133
+ finish_reason: 'stop', model: String(run.model), usage: { total_tokens: 0, completion_tokens: 0, prompt_tokens: 0, cached_tokens: 0 }, error: null,
134
+ temperature: sampling.temperature ?? 1, max_completion_tokens: sampling.max_completions_tokens ?? null, top_p: sampling.top_p ?? 1, seed: sampling.seed ?? 0,
135
+ },
136
+ }));
137
+ }
138
+ const listOutputItems = async (ctx) => {
139
+ const id = at(ctx, 'run_id');
140
+ await progress(ctx, RUN, id, graded(ctx));
141
+ const run = ctx.get(RUN, id);
142
+ if (!run || run.eval_id !== at(ctx, 'eval_id'))
143
+ return ctx.notFound(RUN, id);
144
+ return page(ctx, outputItems(ctx, run));
145
+ };
146
+ // cancelling stops a run still being graded
147
+ const cancelRun = async (ctx) => {
148
+ const id = at(ctx, 'run_id');
149
+ const run = ctx.row(RUN, id);
150
+ if (!run || run.eval_id !== at(ctx, 'eval_id'))
151
+ return ctx.notFound(RUN, id);
152
+ const refusal = ctx.legal(RUN, 'status', 'cancelEvalRun', run.status, 'canceled');
153
+ if (refusal)
154
+ return ctx.refuse(refusal);
155
+ return ctx.reply(await ctx.write(RUN, id, { status: 'canceled' }, 'eval_run.cancel'));
156
+ };
157
+ // an eval's runs, each one's grading observed first
158
+ const listRuns = async (ctx) => {
159
+ const evalId = at(ctx, 'eval_id');
160
+ await progressAll(ctx, RUN, graded(ctx), (r) => r.eval_id === evalId);
161
+ return coreAnswer(ctx);
162
+ };
163
+ export const evals = {
164
+ createEval,
165
+ createEvalRun: createRun,
166
+ getEvalRunOutputItems: listOutputItems,
167
+ cancelEvalRun: cancelRun,
168
+ getEvalRun: async (ctx) => {
169
+ await progress(ctx, RUN, at(ctx, 'run_id'), graded(ctx));
170
+ return coreAnswer(ctx);
171
+ },
172
+ getEvalRuns: listRuns,
173
+ };
@@ -0,0 +1,13 @@
1
+ import type { Semantics, SemanticsContext } from '@volter/world-core';
2
+ /** When a file expires (its `expires_at`): `expires_after.seconds` past its creation when the upload set one, else
3
+ * thirty days for a batch file, else never. "By default, files with `purpose=batch` expire after 30 days and all
4
+ * other files are persisted until they are manually deleted"; `seconds` "must be between 3600 (1 hour) and 2592000
5
+ * (30 days)" (developers.openai.com/api/reference/resources/files/methods/create, `expires_after`). Undefined for a
6
+ * policy OpenAI refuses. */
7
+ export declare function expiresAt(purpose: string, created: number, after: unknown): number | null | undefined;
8
+ /** Files whose time is up are gone, each deleted at the moment it expired, not when a later request looks: a batch
9
+ * input thirty days after its upload (above), a batch's output "automatically deleted 30 days after the batch is
10
+ * complete" (developers.openai.com/api/docs/guides/batch). A get, download or delete of one then answers OpenAI's
11
+ * 404 for a file that does not exist. */
12
+ export declare function expireFiles(ctx: SemanticsContext): Promise<void>;
13
+ export declare const files: Record<string, Semantics>;
@@ -0,0 +1,59 @@
1
+ import { epoch, invalid } from "./shared.js";
2
+ const THIRTY_DAYS = 30 * 24 * 3600;
3
+ /** When a file expires (its `expires_at`): `expires_after.seconds` past its creation when the upload set one, else
4
+ * thirty days for a batch file, else never. "By default, files with `purpose=batch` expire after 30 days and all
5
+ * other files are persisted until they are manually deleted"; `seconds` "must be between 3600 (1 hour) and 2592000
6
+ * (30 days)" (developers.openai.com/api/reference/resources/files/methods/create, `expires_after`). Undefined for a
7
+ * policy OpenAI refuses. */
8
+ export function expiresAt(purpose, created, after) {
9
+ return after !== undefined ? expiresAfter(created, after) : purpose === 'batch' ? created + THIRTY_DAYS : null;
10
+ }
11
+ /** The expiry an upload's own `expires_after` sets, or undefined for one out of range. */
12
+ function expiresAfter(created, after) {
13
+ const a = (after && typeof after === 'object' ? after : {});
14
+ const seconds = Number(a.seconds);
15
+ return a.anchor === 'created_at' && Number.isInteger(seconds) && seconds >= 3600 && seconds <= THIRTY_DAYS ? created + seconds : undefined;
16
+ }
17
+ /** Files whose time is up are gone, each deleted at the moment it expired, not when a later request looks: a batch
18
+ * input thirty days after its upload (above), a batch's output "automatically deleted 30 days after the batch is
19
+ * complete" (developers.openai.com/api/docs/guides/batch). A get, download or delete of one then answers OpenAI's
20
+ * 404 for a file that does not exist. */
21
+ export async function expireFiles(ctx) {
22
+ const now = epoch(ctx);
23
+ const due = ctx.rowsRaw('OpenAIFile').filter((f) => typeof f.expires_at === 'number' && f.expires_at <= now);
24
+ for (const f of due.sort((a, b) => Number(a.expires_at) - Number(b.expires_at))) {
25
+ const at = await ctx.at(new Date(Number(f.expires_at) * 1000).toISOString());
26
+ await at.write('OpenAIFile', String(f.id), { deleted: true }, 'file.delete');
27
+ }
28
+ }
29
+ // The SDK sends multipart/form-data (the part `file`); an in-process caller may send JSON
30
+ // { purpose, filename, content, bytes } instead.
31
+ const create = async (ctx) => {
32
+ const p = ctx.params;
33
+ if (p.purpose === undefined || p.purpose === '')
34
+ return invalid(ctx, 'you must provide a purpose parameter', 'purpose');
35
+ const upload = p.file && typeof p.file === 'object' ? p.file : undefined;
36
+ const content = upload ? String(upload.content ?? '') : typeof p.content === 'string' ? p.content : '';
37
+ const filename = upload ? upload.name || 'upload' : typeof p.filename === 'string' && p.filename ? p.filename : 'upload.jsonl';
38
+ const bytes = upload ? upload.size || content.length : Number(p.bytes ?? (typeof p.content === 'string' ? p.content.length : 0));
39
+ // the SDK sends the policy as the form fields expires_after[anchor] and expires_after[seconds]
40
+ const after = p.expires_after ?? (p['expires_after[anchor]'] === undefined ? undefined : { anchor: p['expires_after[anchor]'], seconds: p['expires_after[seconds]'] });
41
+ const expires = expiresAt(String(p.purpose), epoch(ctx), after);
42
+ if (expires === undefined)
43
+ return invalid(ctx, "'expires_after' must have anchor 'created_at' and seconds between 3600 and 2592000", 'expires_after');
44
+ const id = ctx.mint('OpenAIFile');
45
+ const fields = { object: 'file', bytes: Number.isFinite(bytes) ? bytes : 0, created_at: epoch(ctx), expires_at: expires, filename, purpose: String(p.purpose), status: 'processed', _content: content };
46
+ return ctx.reply(await ctx.write('OpenAIFile', id, fields, 'file.create'));
47
+ };
48
+ // the file's bytes as uploaded, not JSON
49
+ const download = async (ctx) => {
50
+ const id = String(ctx.id);
51
+ const row = ctx.row('OpenAIFile', id);
52
+ if (!row)
53
+ return ctx.notFound('OpenAIFile', id);
54
+ return ctx.raw(String(row._content ?? ''), { headers: { 'content-type': 'application/octet-stream' } });
55
+ };
56
+ export const files = {
57
+ createFile: create,
58
+ downloadFile: download,
59
+ };
@@ -0,0 +1,4 @@
1
+ import type { Semantics, SemanticsContext } from '@volter/world-core';
2
+ export declare const fineTuning: Record<string, Semantics>;
3
+ /** Every job OpenAI is still training, observed (a read observes all the vendor's work before it answers). */
4
+ export declare function observeJobs(ctx: SemanticsContext): Promise<void>;
@@ -0,0 +1,178 @@
1
+ import { fineTuningClosed } from "../openai-models.js";
2
+ import { estimateTokens } from "../openai-stub.js";
3
+ import { coreAnswer, inFlight, progress } from "./progress.js";
4
+ import { epoch, invalid, page } from "./shared.js";
5
+ const JOB = 'FineTuningJob';
6
+ /** The text of a file the job names, or '' when there is none. */
7
+ const fileText = (ctx, id) => String(ctx.row('OpenAIFile', String(id ?? ''), { withDeleted: true })?._content ?? '');
8
+ /** The epochs a job trains: its method's n_epochs, one when left to OpenAI ("auto"). */
9
+ function epochs(row) {
10
+ const m = (row.method ?? {});
11
+ const n = Number(m[String(row.method?.type)]?.hyperparameters?.n_epochs);
12
+ return Number.isInteger(n) && n > 0 ? n : 1;
13
+ }
14
+ /** What a finished job carries: the model it trained, the tokens it trained on (its training file's, once an epoch),
15
+ * and its result file, the training metrics OpenAI writes for a succeeded job
16
+ * (https://platform.openai.com/docs/api-reference/fine-tuning/object, `result_files`, `trained_tokens`). */
17
+ const finish = (ctx) => (row, end) => (end === 'succeeded'
18
+ ? { fine_tuned_model: `ft:${String(row.model)}:twin::${String(row.id)}`, trained_tokens: estimateTokens(fileText(ctx, row.training_file)) * epochs(row), result_files: [`file-twin-ftresult-${String(row.id)}`], ...resolved(row) }
19
+ : {});
20
+ /** A trained job's hyperparameters as it trained with them: each `auto` resolved to a value, as a finished job answers
21
+ * them (the reference's retrieve example: `"hyperparameters": {"n_epochs": 4, "batch_size": 1, "learning_rate_multiplier": 1}`).
22
+ * The twin trains one example a step at the base rate, for the epochs asked or one. */
23
+ function resolved(row) {
24
+ const method = row.method;
25
+ const type = String(method?.type);
26
+ const body = method?.[type];
27
+ if (!method || !body || (type !== 'supervised' && type !== 'dpo'))
28
+ return {};
29
+ const h = (body.hyperparameters ?? {});
30
+ const auto = (v, n) => (v === 'auto' || v === undefined ? n : v);
31
+ const hyperparameters = { ...h, batch_size: auto(h.batch_size, 1), learning_rate_multiplier: auto(h.learning_rate_multiplier, 1), n_epochs: epochs(row) };
32
+ return { method: { ...method, [type]: { ...body, hyperparameters } }, ...(type === 'supervised' ? { hyperparameters } : {}) };
33
+ }
34
+ /** Observe OpenAI's training of a job; the read that finishes it writes its result file (step metrics, stubs). */
35
+ async function train(ctx, id) {
36
+ const moved = await progress(ctx, JOB, id, finish(ctx));
37
+ if (moved?.to !== 'succeeded')
38
+ return;
39
+ const content = 'step,train_loss,train_accuracy,valid_loss,valid_mean_token_accuracy\n1,0,1,,\n';
40
+ await ctx.write('OpenAIFile', `file-twin-ftresult-${id}`, { object: 'file', bytes: content.length, created_at: epoch(ctx), expires_at: null, filename: 'step_metrics.csv', purpose: 'fine-tune-results', status: 'processed', _content: content }, 'file.create');
41
+ }
42
+ /** The job the path names, its training observed, or OpenAI's answer that there is none. */
43
+ async function job(ctx) {
44
+ const id = String(ctx.id);
45
+ await train(ctx, id);
46
+ const found = ctx.get(JOB, id);
47
+ return found ? { job: found } : { answer: ctx.notFound(JOB, id) };
48
+ }
49
+ /** A method's hyperparameters with OpenAI's `auto` for each one not sent (the job object's `method`:
50
+ * https://platform.openai.com/docs/api-reference/fine-tuning/object); a reinforcement method's are its own. */
51
+ function withDefaults(method) {
52
+ const type = String(method.type);
53
+ if (type !== 'supervised' && type !== 'dpo' && type !== 'reinforcement')
54
+ return method;
55
+ const body = (method[type] && typeof method[type] === 'object' ? method[type] : {});
56
+ const sent = (body.hyperparameters && typeof body.hyperparameters === 'object' ? body.hyperparameters : {});
57
+ // a reinforcement job also evaluates on its own schedule and budget, and answers its response format, null unless
58
+ // set (the reference's Reinforcement example)
59
+ const own = type === 'dpo' ? { beta: 'auto' } : type === 'reinforcement' ? { eval_interval: 'auto', eval_samples: 'auto', compute_multiplier: 'auto', reasoning_effort: 'default' } : {};
60
+ const hyperparameters = { batch_size: 'auto', learning_rate_multiplier: 'auto', n_epochs: 'auto', ...own, ...sent };
61
+ return { ...method, [type]: { ...body, hyperparameters, ...(type === 'reinforcement' ? { response_format: body.response_format ?? null } : {}) } };
62
+ }
63
+ const create = async (ctx) => {
64
+ const p = ctx.params;
65
+ if (p.model === undefined || p.model === '')
66
+ return invalid(ctx, 'you must provide a model parameter', 'model');
67
+ if (p.training_file === undefined || p.training_file === '')
68
+ return invalid(ctx, 'you must provide a training_file parameter', 'training_file');
69
+ // fine-tuning's dated availability (../openai-models.ts): whether the organization has fine-tuned before, and when it
70
+ // last ran inference on a fine-tuned model
71
+ const inference = ctx.rowsRaw('_usage_record').filter((r) => String(r.model).startsWith('ft:')).map((r) => Number(r.created_at));
72
+ const closed = fineTuningClosed(epoch(ctx), { everFineTuned: ctx.rowsRaw(JOB, { withDeleted: true }).length > 0, ...(inference.length ? { lastFineTunedInference: Math.max(...inference) } : {}) });
73
+ if (closed)
74
+ return ctx.refuse({ status: 403, message: closed, code: 'fine_tuning_unavailable' });
75
+ const id = ctx.mint(JOB);
76
+ const created = epoch(ctx);
77
+ // the method trained with, supervised by default; the deprecated top-level hyperparameters are a supervised job's
78
+ // method's, and null for any other method (the reference's Epochs and DPO examples)
79
+ const method = withDefaults(p.method && typeof p.method === 'object' ? p.method : { type: 'supervised', supervised: { hyperparameters: p.hyperparameters ?? {} } });
80
+ const hyperparameters = method.type === 'supervised' ? method.supervised.hyperparameters : null;
81
+ // a Weights & Biases run is named by the job unless the request names it (the spec's FineTuningIntegration: "If not
82
+ // set, we will use the Job ID as the name"); its entity is the W&B default unless set
83
+ const integrations = Array.isArray(p.integrations)
84
+ ? p.integrations.map((i) => (i && i.type === 'wandb' ? { ...i, wandb: { entity: null, ...i.wandb, run_id: id } } : i))
85
+ : [];
86
+ const fields = {
87
+ object: 'fine_tuning.job',
88
+ model: String(p.model),
89
+ created_at: created,
90
+ finished_at: null,
91
+ fine_tuned_model: null,
92
+ organization_id: 'org-twin',
93
+ status: 'validating_files',
94
+ training_file: String(p.training_file),
95
+ validation_file: p.validation_file ?? null,
96
+ hyperparameters,
97
+ method,
98
+ integrations,
99
+ metadata: p.metadata && typeof p.metadata === 'object' ? p.metadata : null,
100
+ result_files: [],
101
+ trained_tokens: null,
102
+ // no failure, as the reference's create examples answer it
103
+ error: { code: null, message: null, param: null },
104
+ estimated_finish: null,
105
+ // the suffix it was given, its usage (none until it trains) and whether its data is shared with OpenAI, as the
106
+ // reference's create examples answer them (spec/patches.json adds them to the job object)
107
+ user_provided_suffix: typeof p.suffix === 'string' ? p.suffix : null,
108
+ usage_metrics: null,
109
+ shared_with_openai: false,
110
+ seed: Number(p.seed ?? 0),
111
+ };
112
+ return ctx.reply(await ctx.write(JOB, id, fields, 'fine_tuning_job.create'));
113
+ };
114
+ // the job's event stream: created, then completed once it has succeeded
115
+ const events = async (ctx) => {
116
+ const found = await job(ctx);
117
+ if ('answer' in found)
118
+ return found.answer;
119
+ const j = found.job;
120
+ const created = Number(j.created_at ?? 0);
121
+ const data = [
122
+ // a message event carries no data (https://platform.openai.com/docs/api-reference/fine-tuning/event-object, `data`)
123
+ { object: 'fine_tuning.job.event', id: `ftevent-${j.id}-1`, created_at: created, level: 'info', message: 'Created fine-tuning job', data: null, type: 'message' },
124
+ ...(j.status === 'succeeded' ? [{ object: 'fine_tuning.job.event', id: `ftevent-${j.id}-2`, created_at: Number(j.finished_at ?? created), level: 'info', message: 'Fine-tuning job successfully completed (twin stub)', data: null, type: 'message' }] : []),
125
+ ];
126
+ return ctx.reply({ object: 'list', data, has_more: false });
127
+ };
128
+ // a succeeded job has one final checkpoint naming its fine-tuned model (metrics are stubs); any
129
+ // other job has none
130
+ const checkpoints = async (ctx) => {
131
+ const found = await job(ctx);
132
+ if ('answer' in found)
133
+ return found.answer;
134
+ const j = found.job;
135
+ const data = j.status === 'succeeded'
136
+ ? [{
137
+ object: 'fine_tuning.job.checkpoint',
138
+ id: `ftckpt-${j.id}-1`,
139
+ created_at: Number(j.created_at ?? 0),
140
+ fine_tuned_model_checkpoint: String(j.fine_tuned_model ?? `ft:twin::${j.id}:ckpt-step-1`),
141
+ fine_tuning_job_id: String(j.id),
142
+ metrics: { step: 1, train_loss: 0, train_mean_token_accuracy: 1, full_valid_loss: 0, full_valid_mean_token_accuracy: 1 },
143
+ step_number: 1,
144
+ }]
145
+ : [];
146
+ return ctx.reply({ object: 'list', data, has_more: false, first_id: data[0]?.id ?? null, last_id: data[0]?.id ?? null });
147
+ };
148
+ // the organization's jobs, each one's training observed first, newest first; `metadata[k]=v` keeps the jobs whose
149
+ // metadata holds each pair (https://platform.openai.com/docs/api-reference/fine-tuning/list, `metadata`)
150
+ const listJobs = async (ctx) => {
151
+ for (const r of ctx.rowsRaw(JOB))
152
+ if (inFlight(JOB, r))
153
+ await train(ctx, String(r.id));
154
+ const wanted = Object.entries(ctx.params).flatMap(([k, v]) => { const m = /^metadata\[(.+)\]$/.exec(k); return m ? [[m[1], String(v)]] : []; });
155
+ const nested = ctx.params.metadata && typeof ctx.params.metadata === 'object' ? Object.entries(ctx.params.metadata).map(([k, v]) => [k, String(v)]) : [];
156
+ const pairs = [...wanted, ...nested];
157
+ if (!pairs.length)
158
+ return coreAnswer(ctx);
159
+ const jobs = ctx.rows(JOB).filter((r) => pairs.every(([k, v]) => r.metadata?.[k] === v));
160
+ // newest first, ties by mint order as the derived core lists (a job minted later is newer)
161
+ return page(ctx, jobs.sort((a, b) => Number(b.created_at) - Number(a.created_at) || String(b.id).localeCompare(String(a.id), undefined, { numeric: true })));
162
+ };
163
+ export const fineTuning = {
164
+ createFineTuningJob: create,
165
+ listFineTuningEvents: events,
166
+ listFineTuningJobCheckpoints: checkpoints,
167
+ retrieveFineTuningJob: async (ctx) => {
168
+ await train(ctx, String(ctx.id));
169
+ return coreAnswer(ctx);
170
+ },
171
+ listPaginatedFineTuningJobs: listJobs,
172
+ };
173
+ /** Every job OpenAI is still training, observed (a read observes all the vendor's work before it answers). */
174
+ export async function observeJobs(ctx) {
175
+ for (const row of ctx.rowsRaw(JOB))
176
+ if (inFlight(JOB, row))
177
+ await train(ctx, String(row.id));
178
+ }
@@ -0,0 +1,2 @@
1
+ import type { Semantics } from '@volter/world-core';
2
+ export declare const images: Record<string, Semantics>;
@@ -0,0 +1,18 @@
1
+ import { handleImageEdit, handleImages, handleImageVariation } from "../openai-twin.js";
2
+ import { modelOf } from "../openai-models.js";
3
+ import { recordUsage, send } from "./shared.js";
4
+ /** The images, or their events when the request streamed them; each call made is billed by its images, of the size
5
+ * asked, from its source (the usage report's `images`, `size`, `source`). */
6
+ async function answer(ctx, r, source) {
7
+ if (r.status === 200) {
8
+ const n = (r.body.data ?? []).length;
9
+ const model = modelOf(ctx.call.operation.id, ctx.params);
10
+ await recordUsage(ctx, 'images', model, 0, 0, { images: n, size: typeof ctx.params.size === 'string' ? ctx.params.size : '1024x1024', source });
11
+ }
12
+ return r.events ? ctx.sse(r.events) : send(ctx, r);
13
+ }
14
+ export const images = {
15
+ createImage: async (ctx) => answer(ctx, handleImages(ctx.params, ctx.occurredAt), 'image.generation'),
16
+ createImageEdit: async (ctx) => answer(ctx, handleImageEdit(ctx.params, ctx.occurredAt), 'image.edit'),
17
+ createImageVariation: async (ctx) => answer(ctx, handleImageVariation(ctx.params, ctx.occurredAt), 'image.variation'),
18
+ };
@@ -0,0 +1,8 @@
1
+ import type { Semantics } from '@volter/world-core';
2
+ import type { OpenAIScenarioEngine } from '../openai-scenario.js';
3
+ /** `readOnly`: served by a read-only (mirror) twin, whose reads never write (./progress.ts). */
4
+ export type OpenAISemanticsOptions = {
5
+ scenarioEngine?: OpenAIScenarioEngine;
6
+ readOnly?: boolean;
7
+ };
8
+ export declare function openaiSemantics(options: OpenAISemanticsOptions): Record<string, Semantics>;