@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,13 @@
|
|
|
1
|
+
export type ConformanceViolation = {
|
|
2
|
+
check: string;
|
|
3
|
+
detail: string;
|
|
4
|
+
};
|
|
5
|
+
export type OpenAIConformanceReport = {
|
|
6
|
+
ok: boolean;
|
|
7
|
+
checksRun: number;
|
|
8
|
+
violations: ConformanceViolation[];
|
|
9
|
+
};
|
|
10
|
+
/** Run the offline conformance checks against a fresh temp root. */
|
|
11
|
+
export declare function checkOpenAIConformance(opts?: {
|
|
12
|
+
root?: string;
|
|
13
|
+
}): Promise<OpenAIConformanceReport>;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// OpenAI twin conformance — a lightweight, offline structural check that the twin's served
|
|
2
|
+
// response ENVELOPES match the documented OpenAI shapes. (Unlike the OpenAPI-backed packs,
|
|
3
|
+
// OpenAI's published OpenAPI is huge and not vendored here, so this harness validates the
|
|
4
|
+
// load-bearing envelope SHAPES directly: chat.completion, the streaming chunk sequence,
|
|
5
|
+
// the Responses API, embeddings, models, files, batches, moderations, and the error envelope.
|
|
6
|
+
// Fully offline + deterministic (drives the local handler against a temp root) so it runs in
|
|
7
|
+
// CI without an API key. Honest scope: it checks the protocol envelope, NOT model output
|
|
8
|
+
// (a deterministic stub by design — the labeled stub is the twin's answer).
|
|
9
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
10
|
+
import { tmpdir } from 'node:os';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { handleOpenAITwinRequest } from "./openai-twin.js";
|
|
13
|
+
const isObj = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
14
|
+
/** Run the offline conformance checks against a fresh temp root. */
|
|
15
|
+
export async function checkOpenAIConformance(opts = {}) {
|
|
16
|
+
const root = opts.root ?? mkdtempSync(join(tmpdir(), 'openai-conf-'));
|
|
17
|
+
const owned = opts.root === undefined;
|
|
18
|
+
const violations = [];
|
|
19
|
+
let checksRun = 0;
|
|
20
|
+
const fail = (check, detail) => violations.push({ check, detail });
|
|
21
|
+
const H = (method, path, body, sseSink) => handleOpenAITwinRequest({ method, path, body, root, ...(sseSink ? { sseSink } : {}) });
|
|
22
|
+
try {
|
|
23
|
+
// 1. chat.completion response envelope.
|
|
24
|
+
checksRun++;
|
|
25
|
+
const chat = await H('POST', '/v1/chat/completions', JSON.stringify({ model: 'gpt-4o', messages: [{ role: 'user', content: 'hi' }] }));
|
|
26
|
+
const cb = chat.body;
|
|
27
|
+
if (chat.status !== 200)
|
|
28
|
+
fail('chat.envelope', `status ${chat.status}`);
|
|
29
|
+
else {
|
|
30
|
+
for (const k of ['id', 'object', 'created', 'model', 'choices', 'usage'])
|
|
31
|
+
if (!(k in cb))
|
|
32
|
+
fail('chat.envelope', `missing field "${k}"`);
|
|
33
|
+
if (cb.object !== 'chat.completion')
|
|
34
|
+
fail('chat.envelope', `object is ${String(cb.object)}`);
|
|
35
|
+
const choices = cb.choices;
|
|
36
|
+
if (!Array.isArray(choices) || !isObj(choices[0]) || choices[0].message?.role !== 'assistant')
|
|
37
|
+
fail('chat.envelope', 'choices[0].message not assistant');
|
|
38
|
+
if ((choices[0].finish_reason) !== 'stop')
|
|
39
|
+
fail('chat.envelope', `finish_reason is ${String(choices[0].finish_reason)}`);
|
|
40
|
+
const usage = cb.usage;
|
|
41
|
+
if (!isObj(usage) || typeof usage.prompt_tokens !== 'number' || typeof usage.total_tokens !== 'number')
|
|
42
|
+
fail('chat.usage', 'usage missing prompt/total tokens');
|
|
43
|
+
}
|
|
44
|
+
// 2. chat streaming chunk sequence (ends with done).
|
|
45
|
+
checksRun++;
|
|
46
|
+
const events = [];
|
|
47
|
+
await H('POST', '/v1/chat/completions', JSON.stringify({ model: 'gpt-4o', stream: true, messages: [{ role: 'user', content: 'stream' }] }), (e) => events.push(e));
|
|
48
|
+
const hasRole = events.some((e) => !e.done && e.data.choices?.[0]?.delta?.role === 'assistant');
|
|
49
|
+
const hasDone = events.length > 0 && events[events.length - 1].done === true;
|
|
50
|
+
const firstChunkObj = events[0]?.data?.object;
|
|
51
|
+
if (!hasRole)
|
|
52
|
+
fail('chat.stream', 'no role delta chunk');
|
|
53
|
+
if (!hasDone)
|
|
54
|
+
fail('chat.stream', 'stream did not end with [DONE]');
|
|
55
|
+
if (firstChunkObj !== 'chat.completion.chunk')
|
|
56
|
+
fail('chat.stream', `first chunk object is ${String(firstChunkObj)}`);
|
|
57
|
+
// 3. tool_calls envelope when tools provided.
|
|
58
|
+
checksRun++;
|
|
59
|
+
const tool = await H('POST', '/v1/chat/completions', JSON.stringify({ model: 'gpt-4o', tools: [{ type: 'function', function: { name: 'get_weather', parameters: { type: 'object' } } }], messages: [{ role: 'user', content: 'weather?' }] }));
|
|
60
|
+
const tc = tool.body.choices;
|
|
61
|
+
if ((tc?.[0]?.finish_reason) !== 'tool_calls')
|
|
62
|
+
fail('chat.tool_calls', 'finish_reason not tool_calls');
|
|
63
|
+
const calls = tc?.[0]?.message?.tool_calls;
|
|
64
|
+
if (!Array.isArray(calls) || calls[0]?.type !== 'function')
|
|
65
|
+
fail('chat.tool_calls', 'no function tool_call');
|
|
66
|
+
// 4. Responses API envelope.
|
|
67
|
+
checksRun++;
|
|
68
|
+
const resp = await H('POST', '/v1/responses', JSON.stringify({ model: 'gpt-4o', input: 'hello' }));
|
|
69
|
+
const rb = resp.body;
|
|
70
|
+
if (rb.object !== 'response' || !Array.isArray(rb.output) || rb.output[0]?.type !== 'message')
|
|
71
|
+
fail('responses.envelope', 'bad responses envelope');
|
|
72
|
+
// 5. embeddings shape + determinism.
|
|
73
|
+
checksRun++;
|
|
74
|
+
const e1 = await H('POST', '/v1/embeddings', JSON.stringify({ model: 'text-embedding-3-small', input: 'vectorize me' }));
|
|
75
|
+
const e2 = await H('POST', '/v1/embeddings', JSON.stringify({ model: 'text-embedding-3-small', input: 'vectorize me' }));
|
|
76
|
+
const v1 = e1.body.data?.[0]?.embedding;
|
|
77
|
+
const v2 = e2.body.data?.[0]?.embedding;
|
|
78
|
+
if (!Array.isArray(v1) || v1.length === 0)
|
|
79
|
+
fail('embeddings.shape', 'no embedding vector');
|
|
80
|
+
else if (JSON.stringify(v1) !== JSON.stringify(v2))
|
|
81
|
+
fail('embeddings.deterministic', 'same input produced different vectors');
|
|
82
|
+
// 6. models list + retrieve.
|
|
83
|
+
checksRun++;
|
|
84
|
+
const models = await H('GET', '/v1/models');
|
|
85
|
+
if (!Array.isArray(models.body.data))
|
|
86
|
+
fail('models.list', 'no data array');
|
|
87
|
+
const one = await H('GET', '/v1/models/gpt-4o');
|
|
88
|
+
if (one.body.object !== 'model')
|
|
89
|
+
fail('models.retrieve', 'object is not a model');
|
|
90
|
+
// 7. files create + batches create + moderations.
|
|
91
|
+
checksRun++;
|
|
92
|
+
const file = await H('POST', '/v1/files', JSON.stringify({ purpose: 'batch', filename: 'in.jsonl', content: '{}' }));
|
|
93
|
+
if (file.body.object !== 'file')
|
|
94
|
+
fail('files.create', 'object is not a file');
|
|
95
|
+
const batch = await H('POST', '/v1/batches', JSON.stringify({ input_file_id: 'file-x', endpoint: '/v1/chat/completions', completion_window: '24h' }));
|
|
96
|
+
if (batch.body.object !== 'batch')
|
|
97
|
+
fail('batches.create', 'object is not a batch');
|
|
98
|
+
const mod = await H('POST', '/v1/moderations', JSON.stringify({ input: 'hello' }));
|
|
99
|
+
if (!Array.isArray(mod.body.results))
|
|
100
|
+
fail('moderations', 'no results array');
|
|
101
|
+
// 8. vendor-shaped error envelope.
|
|
102
|
+
checksRun++;
|
|
103
|
+
const bad = await H('POST', '/v1/chat/completions', JSON.stringify({ messages: [{ role: 'user', content: 'hi' }] }));
|
|
104
|
+
const eb = bad.body;
|
|
105
|
+
if (bad.status !== 400 || !isObj(eb.error) || eb.error.type !== 'invalid_request_error')
|
|
106
|
+
fail('error.envelope', 'missing model did not yield a 400 invalid_request_error envelope');
|
|
107
|
+
const nf = await H('GET', '/v1/nonexistent');
|
|
108
|
+
if (nf.status !== 404 || !isObj(nf.body.error))
|
|
109
|
+
fail('error.not_found', 'unknown route did not 404');
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
if (owned)
|
|
113
|
+
rmSync(root, { recursive: true, force: true });
|
|
114
|
+
}
|
|
115
|
+
return { ok: violations.length === 0, checksRun, violations };
|
|
116
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { type RemoteExecute, type PerformContext, type PushOutcome } from '@volter/world-core';
|
|
2
|
+
import type { SyncResource, TwinAction } from '@volter/world-core';
|
|
3
|
+
import { OpenAIBudget, type OpenAIBudgetOptions } from './openai-budget.js';
|
|
4
|
+
/**
|
|
5
|
+
* The injected real-OpenAI boundary. `request` issues ONE OpenAI REST call:
|
|
6
|
+
* method — 'GET' | 'POST' | 'DELETE'
|
|
7
|
+
* path — e.g. '/v1/files' or '/v1/batches/batch_123/cancel'
|
|
8
|
+
* body — JSON body for POST (omitted otherwise)
|
|
9
|
+
* Returns the parsed JSON (an object, a `{ data }` list, or an `{ error }` envelope).
|
|
10
|
+
*/
|
|
11
|
+
export type OpenAIExecute = (method: 'GET' | 'POST' | 'DELETE', path: string, body?: Record<string, unknown>) => Promise<{
|
|
12
|
+
data?: any;
|
|
13
|
+
error?: {
|
|
14
|
+
message?: string;
|
|
15
|
+
type?: string;
|
|
16
|
+
};
|
|
17
|
+
[k: string]: unknown;
|
|
18
|
+
}>;
|
|
19
|
+
/** Construction options for the live executor. `budget` cannot be null and cannot be loosened. */
|
|
20
|
+
export type LiveOpenAIOptions = {
|
|
21
|
+
/** Injected `fetch`, so a test can COUNT the requests the guard did or did not let through. */
|
|
22
|
+
fetchImpl?: typeof fetch;
|
|
23
|
+
/** An existing budget to share across executors. Omit and one is constructed. Cannot be null. */
|
|
24
|
+
budget?: OpenAIBudget;
|
|
25
|
+
/** Construction options for the default budget (ledger path, clock). Cannot loosen it. */
|
|
26
|
+
budgetOptions?: OpenAIBudgetOptions;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* A live executor against the real OpenAI REST API (the user's own API key). Sends the
|
|
30
|
+
* required `Authorization: Bearer` header. Never imported by the pack's own code path — only
|
|
31
|
+
* constructed by a caller that opts into real I/O.
|
|
32
|
+
*
|
|
33
|
+
* THIS IS THE ONE PLACE this pack issues a live `api.openai.com` request, and therefore the one
|
|
34
|
+
* place the rate budget has to be enforced. EVERY call is guarded: the budget is charged BEFORE the
|
|
35
|
+
* request goes out (`checkBudget`, which THROWS `OpenAIBudgetError` instead of returning when the
|
|
36
|
+
* ceiling or a cooldown says stop) and the response is fed back (`recordCall`) so a `retry-after` /
|
|
37
|
+
* 429 / `x-ratelimit-remaining-requests: 0` signal becomes a persisted cooldown that makes every
|
|
38
|
+
* later call fail fast WITHOUT touching OpenAI. There is deliberately no OPTION to disable the guard, and no
|
|
39
|
+
* value a caller can pass for `budget` that yields an unguarded client. What that does NOT claim is
|
|
40
|
+
* immunity from a caller who WANTS one: a fresh `budgetOptions.path` per construction, or an injected
|
|
41
|
+
* clock, restores the allowance, because the same seam tests need cannot be denied to a determined
|
|
42
|
+
* caller in the same process. The kernel header says so up front and this does not upgrade it — see `openai-budget.ts` for why, and for the limits of the guarantee.
|
|
43
|
+
*/
|
|
44
|
+
export declare function liveOpenAIExecute(apiKey: string, base?: string, opts?: LiveOpenAIOptions): OpenAIExecute;
|
|
45
|
+
/** Map a real-OpenAI File object → a twin sync resource. */
|
|
46
|
+
export declare function mapFile(f: Record<string, unknown>): SyncResource;
|
|
47
|
+
/** Map a real-OpenAI Batch object → a twin sync resource. */
|
|
48
|
+
export declare function mapBatch(b: Record<string, unknown>): SyncResource;
|
|
49
|
+
/** Map a real-OpenAI fine-tuning job → a twin sync resource. */
|
|
50
|
+
export declare function mapFineTune(j: Record<string, unknown>): SyncResource;
|
|
51
|
+
/** Map a real-OpenAI vector store → a twin sync resource. */
|
|
52
|
+
export declare function mapVectorStore(v: Record<string, unknown>): SyncResource;
|
|
53
|
+
/** Pull all modeled real collections via the executor and map them to twin sync resources. */
|
|
54
|
+
export declare function pullOpenAIState(execute: OpenAIExecute): Promise<SyncResource[]>;
|
|
55
|
+
/**
|
|
56
|
+
* Pull from real OpenAI and fold into the twin (mirror seeding). syncPull's shadow-diff makes
|
|
57
|
+
* a re-pull of identical state a no-op.
|
|
58
|
+
*/
|
|
59
|
+
export declare function syncOpenAIFromReal(execute: OpenAIExecute, opts: {
|
|
60
|
+
root?: string;
|
|
61
|
+
occurredAt: string;
|
|
62
|
+
}): Promise<{
|
|
63
|
+
observed: number;
|
|
64
|
+
deltasAppended: number;
|
|
65
|
+
}>;
|
|
66
|
+
/**
|
|
67
|
+
* Resolve the REST (method, path) for ONE pending action — faithful to the real OpenAI REST
|
|
68
|
+
* surface for every write op the twin records:
|
|
69
|
+
* - <type>.create → POST <collection>
|
|
70
|
+
* - <type>.cancel → POST <collection>/:id/cancel
|
|
71
|
+
* - <type>.delete → DELETE <collection>/:id
|
|
72
|
+
*/
|
|
73
|
+
export declare function openaiRequestForAction(action: Pick<TwinAction, 'operation' | 'subject'>): {
|
|
74
|
+
method: 'GET' | 'POST' | 'DELETE';
|
|
75
|
+
path: string;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Push ONE pending action to REAL OpenAI via the injected executor. Returns the real external
|
|
79
|
+
* id (the object id from the response; for a create that's a freshly minted id, otherwise it
|
|
80
|
+
* echoes the subject). WRITES TO THE REAL ACCOUNT.
|
|
81
|
+
*/
|
|
82
|
+
export declare function pushOpenAIAction(execute: OpenAIExecute, action: Pick<TwinAction, 'operation' | 'subject' | 'fields'>): Promise<{
|
|
83
|
+
externalId: string;
|
|
84
|
+
}>;
|
|
85
|
+
export declare function openaiExecuteOver(execute: RemoteExecute): OpenAIExecute;
|
|
86
|
+
export declare function performOpenAIAction(execute: RemoteExecute, action: TwinAction, _ctx: PerformContext): Promise<PushOutcome>;
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
// OpenAI CONNECTOR — the live-vendor pull/push path that gives the OpenAI twin the full
|
|
2
|
+
// "git for SaaS" lifecycle (pull real state → mirror; push local writes → real).
|
|
3
|
+
//
|
|
4
|
+
// PULL (real → twin): fetch real Files / Batches / Fine-tuning jobs / Vector stores, map
|
|
5
|
+
// snake_case → SyncResource[], fold into the event log via syncPull
|
|
6
|
+
// (shadow-diff dedup, so re-pulling identical state appends nothing).
|
|
7
|
+
// PUSH (twin → real): for every PENDING local action (create / cancel / delete), call the
|
|
8
|
+
// real OpenAI REST API and confirmAction on success (records the
|
|
9
|
+
// confirmed fields as an observed event + suppresses the local
|
|
10
|
+
// projection — counted exactly once).
|
|
11
|
+
//
|
|
12
|
+
// The vendor I/O is an INJECTED executor (B3 auth boundary): the kernel + this pack hold NO
|
|
13
|
+
// OpenAI key and import NO SDK at runtime. Tests pass a fake executor; live runs pass
|
|
14
|
+
// `liveOpenAIExecute(apiKey)`. Same code path either way — fully exercisable offline.
|
|
15
|
+
import { assertBudgetGuardIntact, observeResource } from '@volter/world-core';
|
|
16
|
+
import { OpenAIBudget, OpenAIBudgetError, openaiCallWeight } from "./openai-budget.js";
|
|
17
|
+
const SERVICE = 'openai';
|
|
18
|
+
/**
|
|
19
|
+
* A live executor against the real OpenAI REST API (the user's own API key). Sends the
|
|
20
|
+
* required `Authorization: Bearer` header. Never imported by the pack's own code path — only
|
|
21
|
+
* constructed by a caller that opts into real I/O.
|
|
22
|
+
*
|
|
23
|
+
* THIS IS THE ONE PLACE this pack issues a live `api.openai.com` request, and therefore the one
|
|
24
|
+
* place the rate budget has to be enforced. EVERY call is guarded: the budget is charged BEFORE the
|
|
25
|
+
* request goes out (`checkBudget`, which THROWS `OpenAIBudgetError` instead of returning when the
|
|
26
|
+
* ceiling or a cooldown says stop) and the response is fed back (`recordCall`) so a `retry-after` /
|
|
27
|
+
* 429 / `x-ratelimit-remaining-requests: 0` signal becomes a persisted cooldown that makes every
|
|
28
|
+
* later call fail fast WITHOUT touching OpenAI. There is deliberately no OPTION to disable the guard, and no
|
|
29
|
+
* value a caller can pass for `budget` that yields an unguarded client. What that does NOT claim is
|
|
30
|
+
* immunity from a caller who WANTS one: a fresh `budgetOptions.path` per construction, or an injected
|
|
31
|
+
* clock, restores the allowance, because the same seam tests need cannot be denied to a determined
|
|
32
|
+
* caller in the same process. The kernel header says so up front and this does not upgrade it — see `openai-budget.ts` for why, and for the limits of the guarantee.
|
|
33
|
+
*/
|
|
34
|
+
export function liveOpenAIExecute(apiKey, base = 'https://api.openai.com', opts = {}) {
|
|
35
|
+
// `null`/`undefined` (or omitting it) build the default budget. Anything else must be an
|
|
36
|
+
// UNMODIFIED OpenAIBudget: a duck-typed stand-in, a SUBCLASS that overrides `checkBudget`, and a
|
|
37
|
+
// Proxy that traps it are all refused, because all three are one-liners that would otherwise
|
|
38
|
+
// hand back a client with no ceiling at all (§9 finding, 2026-07-26 — `instanceof` alone was
|
|
39
|
+
// not a check). What this cannot stop is deliberate sabotage from inside the process (an
|
|
40
|
+
// injected clock, a throwaway ledger path); the kernel's header says so rather than pretending
|
|
41
|
+
// otherwise, and this guards the accident and the one-liner, which are the shapes that happen.
|
|
42
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
43
|
+
// The default ledger is keyed by a hash of THIS key — OpenAI limits per organization/project,
|
|
44
|
+
// so a cwd-scoped ledger would hand the same key a fresh allowance per checkout/worktree/CI leg.
|
|
45
|
+
// ONE expression decides which budget is used, so there is no second, weaker test that could
|
|
46
|
+
// disagree with the first. `null`/`undefined` (or omitting it) build the default; anything else
|
|
47
|
+
// must be an UNMODIFIED OpenAIBudget — a duck-typed stand-in, a SUBCLASS overriding
|
|
48
|
+
// `checkBudget`, and a Proxy trapping it are ALL refused, because each is a one-liner that
|
|
49
|
+
// would otherwise hand back a client with no ceiling (§9 finding, 2026-07-26: `instanceof`
|
|
50
|
+
// alone was not a check — a subclass satisfied it). What this cannot stop is deliberate
|
|
51
|
+
// sabotage from inside the process (an injected clock, a throwaway ledger path); the kernel
|
|
52
|
+
// header states that limit rather than pretending otherwise. This closes the accident and the
|
|
53
|
+
// one-liner, which are the shapes that actually happen.
|
|
54
|
+
const budget = opts.budget !== undefined && opts.budget !== null
|
|
55
|
+
? assertBudgetGuardIntact(opts.budget, OpenAIBudget, 'liveOpenAIExecute')
|
|
56
|
+
: new OpenAIBudget({ token: apiKey, ...(opts.budgetOptions ?? {}) });
|
|
57
|
+
return async (method, path, body) => {
|
|
58
|
+
const headers = { authorization: `Bearer ${apiKey}` };
|
|
59
|
+
const init = { method, headers };
|
|
60
|
+
if (method === 'POST') {
|
|
61
|
+
headers['content-type'] = 'application/json';
|
|
62
|
+
init.body = JSON.stringify(body ?? {});
|
|
63
|
+
}
|
|
64
|
+
const weight = openaiCallWeight(method, path);
|
|
65
|
+
// THROWS instead of calling. Nothing below this line runs when the budget refuses.
|
|
66
|
+
const reservation = budget.checkBudget(weight);
|
|
67
|
+
const res = await doFetch(`${base}${path}`, init);
|
|
68
|
+
const resHeaders = {};
|
|
69
|
+
res.headers.forEach((v, k) => { resHeaders[k.toLowerCase()] = v; });
|
|
70
|
+
const parsed = (await res.json());
|
|
71
|
+
// Settles the reservation and, on a back-off signal, arms the cooldown. May itself throw (a
|
|
72
|
+
// `retry-after` beyond the cap is not something to sleep off) — the cooldown is persisted
|
|
73
|
+
// first either way, so the refusal survives the throw.
|
|
74
|
+
// recordCall may THROW after arming the cooldown (a back-off beyond the cap). On a refused
|
|
75
|
+
// call that louder refusal wins; an answer OpenAI ACCEPTED is kept, so a write that landed is
|
|
76
|
+
// never recorded as failed and performed again on retry.
|
|
77
|
+
try {
|
|
78
|
+
budget.recordCall(weight, resHeaders, { status: res.status, reservation });
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (!(error instanceof OpenAIBudgetError) || !res.ok)
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
return parsed;
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function listOf(res) {
|
|
88
|
+
return Array.isArray(res.data) ? res.data : [];
|
|
89
|
+
}
|
|
90
|
+
function throwIfError(res, ctx) {
|
|
91
|
+
if (res.error)
|
|
92
|
+
throw new Error(`openai ${ctx} failed: ${res.error.message ?? 'unknown error'}`);
|
|
93
|
+
}
|
|
94
|
+
// ── PULL ────────────────────────────────────────────────────────────────────
|
|
95
|
+
/** Map a real-OpenAI File object → a twin sync resource. */
|
|
96
|
+
export function mapFile(f) {
|
|
97
|
+
return {
|
|
98
|
+
type: 'file',
|
|
99
|
+
id: String(f.id),
|
|
100
|
+
fields: {
|
|
101
|
+
object: 'file',
|
|
102
|
+
bytes: f.bytes ?? 0,
|
|
103
|
+
created_at: f.created_at ?? null,
|
|
104
|
+
filename: f.filename ?? null,
|
|
105
|
+
purpose: f.purpose ?? null,
|
|
106
|
+
status: f.status ?? null,
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/** Map a real-OpenAI Batch object → a twin sync resource. */
|
|
111
|
+
export function mapBatch(b) {
|
|
112
|
+
const counts = (b.request_counts && typeof b.request_counts === 'object') ? b.request_counts : {};
|
|
113
|
+
return {
|
|
114
|
+
type: 'batch',
|
|
115
|
+
id: String(b.id),
|
|
116
|
+
fields: {
|
|
117
|
+
object: 'batch',
|
|
118
|
+
endpoint: b.endpoint ?? null,
|
|
119
|
+
input_file_id: b.input_file_id ?? null,
|
|
120
|
+
completion_window: b.completion_window ?? null,
|
|
121
|
+
status: b.status ?? null,
|
|
122
|
+
output_file_id: b.output_file_id ?? null,
|
|
123
|
+
created_at: b.created_at ?? null,
|
|
124
|
+
completed_at: b.completed_at ?? null,
|
|
125
|
+
request_counts: counts,
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/** Map a real-OpenAI fine-tuning job → a twin sync resource. */
|
|
130
|
+
export function mapFineTune(j) {
|
|
131
|
+
return {
|
|
132
|
+
type: 'fine_tuning_job',
|
|
133
|
+
id: String(j.id),
|
|
134
|
+
fields: {
|
|
135
|
+
object: 'fine_tuning.job',
|
|
136
|
+
model: j.model ?? null,
|
|
137
|
+
status: j.status ?? null,
|
|
138
|
+
fine_tuned_model: j.fine_tuned_model ?? null,
|
|
139
|
+
training_file: j.training_file ?? null,
|
|
140
|
+
created_at: j.created_at ?? null,
|
|
141
|
+
finished_at: j.finished_at ?? null,
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/** Map a real-OpenAI vector store → a twin sync resource. */
|
|
146
|
+
export function mapVectorStore(v) {
|
|
147
|
+
const counts = (v.file_counts && typeof v.file_counts === 'object') ? v.file_counts : {};
|
|
148
|
+
return {
|
|
149
|
+
type: 'vector_store',
|
|
150
|
+
id: String(v.id),
|
|
151
|
+
fields: {
|
|
152
|
+
object: 'vector_store',
|
|
153
|
+
name: v.name ?? null,
|
|
154
|
+
status: v.status ?? null,
|
|
155
|
+
created_at: v.created_at ?? null,
|
|
156
|
+
usage_bytes: v.usage_bytes ?? 0,
|
|
157
|
+
file_counts: counts,
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
const COLLECTIONS = [
|
|
162
|
+
{ path: '/v1/files', map: mapFile },
|
|
163
|
+
{ path: '/v1/batches', map: mapBatch },
|
|
164
|
+
{ path: '/v1/fine_tuning/jobs', map: mapFineTune },
|
|
165
|
+
{ path: '/v1/vector_stores', map: mapVectorStore },
|
|
166
|
+
];
|
|
167
|
+
/** Pull all modeled real collections via the executor and map them to twin sync resources. */
|
|
168
|
+
export async function pullOpenAIState(execute) {
|
|
169
|
+
const out = [];
|
|
170
|
+
for (const c of COLLECTIONS) {
|
|
171
|
+
const res = await execute('GET', `${c.path}?limit=100`);
|
|
172
|
+
throwIfError(res, `pull ${c.path}`);
|
|
173
|
+
for (const item of listOf(res))
|
|
174
|
+
out.push(c.map(item));
|
|
175
|
+
}
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Pull from real OpenAI and fold into the twin (mirror seeding). syncPull's shadow-diff makes
|
|
180
|
+
* a re-pull of identical state a no-op.
|
|
181
|
+
*/
|
|
182
|
+
export async function syncOpenAIFromReal(execute, opts) {
|
|
183
|
+
const resources = await pullOpenAIState(execute);
|
|
184
|
+
// PROTOCOL 2: the pack observes each resource; the kernel diffs it against the tree and folds what changed
|
|
185
|
+
let appended = 0;
|
|
186
|
+
for (const r of resources)
|
|
187
|
+
appended += observeResource(SERVICE, { type: r.type, id: r.id, fields: r.fields }, { ...(opts.root !== undefined ? { root: opts.root } : {}), at: opts.occurredAt }).appended;
|
|
188
|
+
return { observed: resources.length, deltasAppended: appended };
|
|
189
|
+
}
|
|
190
|
+
// ── PUSH ────────────────────────────────────────────────────────────────────
|
|
191
|
+
// The twin operations this connector knows how to push to real OpenAI. Anything not here must
|
|
192
|
+
// FAIL LOUDLY rather than be silently dropped — pushing an unrecognized op risks hitting the
|
|
193
|
+
// wrong endpoint or no-op'ing a real change.
|
|
194
|
+
const PUSHABLE_VERBS = new Set(['create', 'cancel', 'delete']);
|
|
195
|
+
/** Throw if `op` is not a write operation this connector can faithfully push. */
|
|
196
|
+
function assertPushable(op) {
|
|
197
|
+
const verb = op.includes('.') ? op.slice(op.indexOf('.') + 1) : op;
|
|
198
|
+
if (!PUSHABLE_VERBS.has(verb)) {
|
|
199
|
+
throw new Error(`openai push: unsupported operation '${op}' — refusing to silently drop a local write`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// Map a subject type → its REST collection path.
|
|
203
|
+
const COLLECTION_PATH = {
|
|
204
|
+
file: '/v1/files',
|
|
205
|
+
batch: '/v1/batches',
|
|
206
|
+
fine_tuning_job: '/v1/fine_tuning/jobs',
|
|
207
|
+
vector_store: '/v1/vector_stores',
|
|
208
|
+
vector_store_file: '/v1/vector_stores',
|
|
209
|
+
};
|
|
210
|
+
/**
|
|
211
|
+
* Resolve the REST (method, path) for ONE pending action — faithful to the real OpenAI REST
|
|
212
|
+
* surface for every write op the twin records:
|
|
213
|
+
* - <type>.create → POST <collection>
|
|
214
|
+
* - <type>.cancel → POST <collection>/:id/cancel
|
|
215
|
+
* - <type>.delete → DELETE <collection>/:id
|
|
216
|
+
*/
|
|
217
|
+
export function openaiRequestForAction(action) {
|
|
218
|
+
const op = action.operation ?? `${action.subject.type}.update`;
|
|
219
|
+
const verb = op.includes('.') ? op.slice(op.indexOf('.') + 1) : op;
|
|
220
|
+
const collection = COLLECTION_PATH[action.subject.type] ?? `/v1/${action.subject.type}s`;
|
|
221
|
+
if (verb === 'create')
|
|
222
|
+
return { method: 'POST', path: collection };
|
|
223
|
+
if (verb === 'cancel')
|
|
224
|
+
return { method: 'POST', path: `${collection}/${action.subject.id}/cancel` };
|
|
225
|
+
if (verb === 'delete')
|
|
226
|
+
return { method: 'DELETE', path: `${collection}/${action.subject.id}` };
|
|
227
|
+
// assertPushable rejects anything else, so this is only reached for the pushable verbs above.
|
|
228
|
+
return { method: 'POST', path: collection };
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Push ONE pending action to REAL OpenAI via the injected executor. Returns the real external
|
|
232
|
+
* id (the object id from the response; for a create that's a freshly minted id, otherwise it
|
|
233
|
+
* echoes the subject). WRITES TO THE REAL ACCOUNT.
|
|
234
|
+
*/
|
|
235
|
+
export async function pushOpenAIAction(execute, action) {
|
|
236
|
+
assertPushable(action.operation ?? `${action.subject.type}.update`);
|
|
237
|
+
const { method, path } = openaiRequestForAction(action);
|
|
238
|
+
const verb = (action.operation ?? '').includes('.') ? action.operation.slice(action.operation.indexOf('.') + 1) : '';
|
|
239
|
+
// On create, push the vendor-accepted create payload (the stored fields include derived
|
|
240
|
+
// lifecycle data the create endpoint does not accept; strip the twin's private fields).
|
|
241
|
+
const payload = verb === 'create' ? createPayload(action) : undefined;
|
|
242
|
+
const res = await execute(method, path, payload);
|
|
243
|
+
throwIfError(res, `push ${action.subject.type}`);
|
|
244
|
+
const id = res.id;
|
|
245
|
+
return { externalId: typeof id === 'string' && id ? id : action.subject.id };
|
|
246
|
+
}
|
|
247
|
+
function createPayload(action) {
|
|
248
|
+
const f = (action.fields ?? {});
|
|
249
|
+
switch (action.subject.type) {
|
|
250
|
+
case 'batch':
|
|
251
|
+
return { input_file_id: f.input_file_id, endpoint: f.endpoint, completion_window: f.completion_window, ...(f.metadata ? { metadata: f.metadata } : {}) };
|
|
252
|
+
case 'fine_tuning_job':
|
|
253
|
+
return { model: f.model, training_file: f.training_file, ...(f.validation_file ? { validation_file: f.validation_file } : {}) };
|
|
254
|
+
case 'vector_store':
|
|
255
|
+
return { ...(f.name ? { name: f.name } : {}) };
|
|
256
|
+
case 'file':
|
|
257
|
+
return { purpose: f.purpose, filename: f.filename };
|
|
258
|
+
default:
|
|
259
|
+
return {};
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// ── THE PERFORM ADAPTER (protocol 2, state-system.ts) ─────────────────────────────────────────
|
|
263
|
+
// One entry against the real account over the kernel executor. A generative vendor's change is
|
|
264
|
+
// the CALL: a recorded completion crosses as the same request (its model and messages), and the
|
|
265
|
+
// vendor's completion id is the receipt. The account-shaped writes (files, batches, fine-tuning
|
|
266
|
+
// jobs, vector stores) cross through `pushOpenAIAction` as before. The credential never enters
|
|
267
|
+
// this file: the executor applies it.
|
|
268
|
+
export function openaiExecuteOver(execute) {
|
|
269
|
+
return async (method, path, body) => {
|
|
270
|
+
const res = await execute({ method, path, headers: { 'content-type': 'application/json' }, ...(body === undefined ? {} : { body: JSON.stringify(body) }) });
|
|
271
|
+
try {
|
|
272
|
+
return JSON.parse(res.body);
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
return { error: { message: `openai answered ${res.status} with a body that is not JSON` } };
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
export async function performOpenAIAction(execute, action, _ctx) {
|
|
280
|
+
if (action.subject.type === 'chat_completion') {
|
|
281
|
+
const f = (action.fields ?? {});
|
|
282
|
+
const messages = Array.isArray(f._input_messages) ? f._input_messages.map((m) => ({ role: m.role, content: m.content })) : [];
|
|
283
|
+
const res = await openaiExecuteOver(execute)('POST', '/v1/chat/completions', { model: f.model, messages, ...(f._stored === true ? { store: true } : {}) });
|
|
284
|
+
throwIfError(res, 'perform chat.completions.create');
|
|
285
|
+
const id = res.id;
|
|
286
|
+
// the vendor's completion is the answer: it rides on the receipt and reaches the app under live use
|
|
287
|
+
return { externalId: typeof id === 'string' && id ? id : action.subject.id, data: res };
|
|
288
|
+
}
|
|
289
|
+
const { externalId } = await pushOpenAIAction(openaiExecuteOver(execute), action);
|
|
290
|
+
return { externalId };
|
|
291
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** The raw bytes of an uploaded file part, as the request's multipart parse keeps them; undefined for a string. */
|
|
2
|
+
export declare function partBytes(v: unknown): Uint8Array | undefined;
|
|
3
|
+
/** An image's format by its signature: png, jpg or webp, else undefined. */
|
|
4
|
+
export declare const imageFormat: (b: Uint8Array) => "png" | "jpg" | "webp" | undefined;
|
|
5
|
+
/** A PNG's width and height from its IHDR chunk. */
|
|
6
|
+
export declare function pngSize(b: Uint8Array): {
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
};
|
|
10
|
+
/** An audio file's format by its signature, among those OpenAI transcribes, else undefined. */
|
|
11
|
+
export declare const audioFormat: (b: Uint8Array) => string | undefined;
|
|
12
|
+
/** A WAV file's length in seconds, from its `fmt ` byte rate and its `data` size (RIFF chunks in any order); undefined
|
|
13
|
+
* for any other format, whose length the twin does not read. */
|
|
14
|
+
export declare function wavSeconds(b: Uint8Array): number | undefined;
|
|
15
|
+
/** The size an image request asks for (`1536x1024`), or the fallback for `auto` or none. */
|
|
16
|
+
export declare function requestedSize(size: unknown, fallback?: {
|
|
17
|
+
width: number;
|
|
18
|
+
height: number;
|
|
19
|
+
}): {
|
|
20
|
+
width: number;
|
|
21
|
+
height: number;
|
|
22
|
+
};
|
|
23
|
+
/** A real PNG of the size asked, one flat colour drawn from `seed`, labeled as the twin's placeholder. */
|
|
24
|
+
export declare function placeholderPng(size: {
|
|
25
|
+
width: number;
|
|
26
|
+
height: number;
|
|
27
|
+
}, seed: string): Uint8Array;
|
|
28
|
+
/** Bytes as base64. */
|
|
29
|
+
export declare function base64(b: Uint8Array): string;
|
|
30
|
+
/** A second of 24 kHz 16-bit mono silence as samples' bytes: `pcm`, raw little-endian with no header. */
|
|
31
|
+
export declare const silentPcm: () => Uint8Array;
|
|
32
|
+
/** The same second as a WAV file. */
|
|
33
|
+
export declare function silentWav(): Uint8Array;
|
|
34
|
+
/** The same second as MP3: MPEG-2 Layer III frames at 24 kHz, 32 kbit/s, mono, each 576 samples in 96 bytes whose
|
|
35
|
+
* side information codes no spectral data, which a decoder plays as silence. */
|
|
36
|
+
export declare function silentMp3(): Uint8Array;
|
|
37
|
+
/** The same second as FLAC: a STREAMINFO block and frames of 4096 samples, each one CONSTANT subframe of zero. */
|
|
38
|
+
export declare function silentFlac(): Uint8Array;
|
|
39
|
+
/** The same second as AAC in ADTS: AAC-LC frames of 1024 samples at 24 kHz, mono, each one channel element with
|
|
40
|
+
* no scale-factor bands, which a decoder plays as silence. */
|
|
41
|
+
export declare function silentAac(): Uint8Array;
|
|
42
|
+
/** The same second as Opus in Ogg: its identification and comment headers, then 20 ms packets of silence. */
|
|
43
|
+
export declare function silentOpus(): Uint8Array;
|