@volter/twin-xai 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +246 -0
- package/client/xai-device-auth.css +246 -0
- package/client/xai-device-auth.tsx +138 -0
- package/dist/client/xai-device-auth.bundle.js +18 -0
- package/dist/client/xai-device-auth.css +246 -0
- package/dist/client/xai-device-auth.d.ts +19 -0
- package/dist/client/xai-device-auth.js +50 -0
- package/dist/client/xai-device-auth.tsx +138 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +28 -0
- package/dist/src/index.d.ts +17 -0
- package/dist/src/index.js +70 -0
- package/dist/src/xai-budget.d.ts +60 -0
- package/dist/src/xai-budget.js +139 -0
- package/dist/src/xai-capabilities.d.ts +4 -0
- package/dist/src/xai-capabilities.js +1072 -0
- package/dist/src/xai-conformance.d.ts +13 -0
- package/dist/src/xai-conformance.js +148 -0
- package/dist/src/xai-connector.d.ts +82 -0
- package/dist/src/xai-connector.js +174 -0
- package/dist/src/xai-device-auth-css.gen.d.ts +1 -0
- package/dist/src/xai-device-auth-css.gen.js +6 -0
- package/dist/src/xai-device-auth-ui.d.ts +13 -0
- package/dist/src/xai-device-auth-ui.js +72 -0
- package/dist/src/xai-models.d.ts +57 -0
- package/dist/src/xai-models.js +102 -0
- package/dist/src/xai-oauth.d.ts +30 -0
- package/dist/src/xai-oauth.js +279 -0
- package/dist/src/xai-scenario.d.ts +33 -0
- package/dist/src/xai-scenario.js +139 -0
- package/dist/src/xai-server.d.ts +36 -0
- package/dist/src/xai-server.js +232 -0
- package/dist/src/xai-stub.d.ts +69 -0
- package/dist/src/xai-stub.js +210 -0
- package/dist/src/xai-twin.d.ts +89 -0
- package/dist/src/xai-twin.js +883 -0
- package/dist/src/xai-types.d.ts +118 -0
- package/dist/src/xai-types.js +6 -0
- package/package.json +76 -0
- package/src/cli.ts +27 -0
- package/src/index.ts +120 -0
- package/src/xai-budget.ts +165 -0
- package/src/xai-capabilities.ts +1046 -0
- package/src/xai-conformance.ts +136 -0
- package/src/xai-connector.ts +212 -0
- package/src/xai-device-auth-css.gen.ts +6 -0
- package/src/xai-device-auth-ui.ts +90 -0
- package/src/xai-journey.uitest.ts +155 -0
- package/src/xai-models.ts +154 -0
- package/src/xai-oauth.ts +301 -0
- package/src/xai-scenario.ts +148 -0
- package/src/xai-server.ts +258 -0
- package/src/xai-stub.ts +213 -0
- package/src/xai-twin.ts +960 -0
- package/src/xai-types.ts +111 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// xAI twin HTTP server — serve the full xAI twin handler over HTTP so the real `@ai-sdk/xai`
|
|
2
|
+
// provider (constructed with `baseURL: http://127.0.0.1:<port>/v1`) works unmodified. JSON
|
|
3
|
+
// bodies pass straight through. Writable by default; pass readOnly to reject mutations (D3).
|
|
4
|
+
//
|
|
5
|
+
// Streaming: when the request body has `"stream": true`, the server constructs a REAL SSE
|
|
6
|
+
// response by feeding the handler an sseSink that writes each chunk onto the HTTP stream in
|
|
7
|
+
// the `data: <json>\n\n` wire format, ending with `data: [DONE]\n\n`. (The handler itself
|
|
8
|
+
// stays socket-free — the sink is the only place a socket is touched, on the live HTTP path.)
|
|
9
|
+
//
|
|
10
|
+
// Scenario scripting (xai-scenario.ts): a JSON scenario file — via the scenarioPath option,
|
|
11
|
+
// the TWIN_XAI_SCENARIO env var, or `world-xai serve --scenario <path>` — scripts the exact
|
|
12
|
+
// assistant turns for POST /v1/chat/completions. One session per server (nthCall/once state).
|
|
13
|
+
import { serveHttp } from '@volter/world-core';
|
|
14
|
+
import { twinManifest, twinPublicBase, worldNow } from '@volter/world-core';
|
|
15
|
+
import { createXaiScenarioEngine, loadXaiScenarioDocument } from "./xai-scenario.js";
|
|
16
|
+
import { handleXaiTwinRequest } from "./xai-twin.js";
|
|
17
|
+
import { decideXaiTwinDeviceAuthorization } from "./xai-oauth.js";
|
|
18
|
+
import { deviceConsentPageHtml, deviceDonePageHtml, deviceEntryPageHtml, xaiDeviceFavicon, xaiDeviceAuthView, xaiDeviceScript, xaiDeviceStylesheet, XAI_DEVICE_FAVICON_PATH, XAI_DEVICE_SCRIPT_PATH, XAI_DEVICE_STYLE_PATH, } from "./xai-device-auth-ui.js";
|
|
19
|
+
function wantsStream(body) {
|
|
20
|
+
if (!body)
|
|
21
|
+
return false;
|
|
22
|
+
try {
|
|
23
|
+
const parsed = JSON.parse(body);
|
|
24
|
+
// deferred takes precedence over stream: the vendor answers a deferred request with
|
|
25
|
+
// { request_id }, not an event stream.
|
|
26
|
+
return parsed?.stream === true && parsed?.deferred !== true;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function encodeSse(event) {
|
|
33
|
+
if (event.done)
|
|
34
|
+
return 'data: [DONE]\n\n';
|
|
35
|
+
return `data: ${JSON.stringify(event.data)}\n\n`;
|
|
36
|
+
}
|
|
37
|
+
const STREAMABLE = new Set(['/v1/chat/completions']);
|
|
38
|
+
function html(body, status = 200) {
|
|
39
|
+
return new Response(body, {
|
|
40
|
+
status,
|
|
41
|
+
headers: {
|
|
42
|
+
'cache-control': 'no-store',
|
|
43
|
+
'content-security-policy': "default-src 'self'; style-src 'self'; img-src 'self' data:; form-action 'self'; frame-ancestors 'none'; base-uri 'none'",
|
|
44
|
+
'content-type': 'text/html; charset=utf-8',
|
|
45
|
+
'referrer-policy': 'no-referrer',
|
|
46
|
+
'x-content-type-options': 'nosniff',
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The pack's whole HTTP surface as a plain `fetch` — Request in, Response out, no listener.
|
|
52
|
+
*
|
|
53
|
+
* This is the composable form (runtime contract R12b): a Worker / Durable Object entry has NO
|
|
54
|
+
* loopback ports, so it must mount a pack's handler IN-PROCESS. `createXaiTwinServer` is
|
|
55
|
+
* nothing but `Bun.serve` wrapped around this closure, so the standalone (R1) and hosted
|
|
56
|
+
* surfaces are the SAME code — there is no second HTTP adaptation to drift. openai is the
|
|
57
|
+
* reference for this shape; xai is its generative sibling.
|
|
58
|
+
*
|
|
59
|
+
* WHAT IT SERVES IS UNCHANGED (R9): xai is a GENERATIVE pack, so chat completions answer a
|
|
60
|
+
* labeled deterministic stub or a scripted scenario — never a model. The only wall-clock-shaped
|
|
61
|
+
* call on this path is `worldNow()`, the world's frozen instant, and GET /twin is built from
|
|
62
|
+
* constants plus the scenario engine's own counters, so replaying it on identical state is
|
|
63
|
+
* byte-identical.
|
|
64
|
+
*
|
|
65
|
+
* NOTHING ON THIS PATH TOUCHES A FILESYSTEM. The scenario document is read through the ACTIVE
|
|
66
|
+
* WORLD STORE (xai-scenario.ts), once, when the factory is called; the device-authorization
|
|
67
|
+
* stylesheet, script and favicon are all served from committed constants
|
|
68
|
+
* (xai-device-auth-ui.ts), so the OAuth protocol UI is workerd-servable too.
|
|
69
|
+
*/
|
|
70
|
+
export function createXaiTwinFetch(options = {}) {
|
|
71
|
+
const readOnly = options.readOnly ?? false;
|
|
72
|
+
const scenarioPath = options.scenarioPath ?? process.env.TWIN_XAI_SCENARIO;
|
|
73
|
+
const scenarioEngine = scenarioPath ? createXaiScenarioEngine(loadXaiScenarioDocument(scenarioPath)) : undefined;
|
|
74
|
+
return async function xaiTwinFetch(request) {
|
|
75
|
+
const url = new URL(request.url);
|
|
76
|
+
const path = url.pathname + (url.search || '');
|
|
77
|
+
const cleanPath = url.pathname.replace(/\/+$/, '');
|
|
78
|
+
// THE READ DOORS (TWIN-PROGRAMMING-MODEL): discovery + inspection, read-only.
|
|
79
|
+
if (request.method === 'GET' && cleanPath === '/twin') {
|
|
80
|
+
return Response.json(twinManifest({
|
|
81
|
+
vendor: 'xai',
|
|
82
|
+
twinOf: 'xAI Grok API (chat completions)',
|
|
83
|
+
stateSentence: 'Seed nothing — usage accrues from ordinary API use with any key.',
|
|
84
|
+
behaviorSentence: 'Completions are scripted by MSW-shaped handlers in the world dir (handlers/xai.json): {on:{userTextIncludes|anyTextIncludes|modelEquals|hasTool|toolResultFor|lastMessageIsToolResult|nthCall}, respond:{text|toolCalls, finishReason?}, once?, phase?}. Unmatched requests answer a labeled stub naming this door.',
|
|
85
|
+
exampleHandler: { on: { userTextIncludes: 'post', hasTool: 'draftPost' }, respond: { toolCalls: { name: 'draftPost', arguments: { text: 'demo' } } }, once: true },
|
|
86
|
+
engine: scenarioEngine,
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
if (request.method === 'GET' && cleanPath === '/twin/scenario') {
|
|
90
|
+
return Response.json(scenarioEngine ? scenarioEngine.status() : { vendor: 'xai', handlers: [], misses: 0, recentMisses: [] });
|
|
91
|
+
}
|
|
92
|
+
if (request.method === 'GET' && cleanPath === XAI_DEVICE_STYLE_PATH) {
|
|
93
|
+
return new Response(xaiDeviceStylesheet(), {
|
|
94
|
+
headers: { 'cache-control': 'no-store', 'content-type': 'text/css; charset=utf-8', 'x-content-type-options': 'nosniff' },
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
if (request.method === 'GET' && (cleanPath === XAI_DEVICE_FAVICON_PATH || cleanPath === '/favicon.ico')) {
|
|
98
|
+
return new Response(xaiDeviceFavicon(), {
|
|
99
|
+
headers: { 'cache-control': 'public, max-age=86400', 'content-type': 'image/svg+xml', 'x-content-type-options': 'nosniff' },
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if (request.method === 'GET' && cleanPath === XAI_DEVICE_SCRIPT_PATH) {
|
|
103
|
+
return new Response(xaiDeviceScript(), {
|
|
104
|
+
headers: { 'cache-control': 'no-store', 'content-type': 'text/javascript; charset=utf-8', 'x-content-type-options': 'nosniff' },
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
// Internal Twin control routes exist for in-process capability setup only. They are never
|
|
108
|
+
// part of the network surface; browser authorization uses the real device forms below.
|
|
109
|
+
if (cleanPath.startsWith('/twin/')) {
|
|
110
|
+
return new Response(JSON.stringify({ code: 'not_found', error: 'The requested endpoint does not exist.' }), {
|
|
111
|
+
status: 404,
|
|
112
|
+
headers: { 'content-type': 'application/json' },
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
if (request.method === 'GET' && cleanPath === '') {
|
|
116
|
+
return Response.redirect(`${twinPublicBase(request)}/oauth2/device`, 303);
|
|
117
|
+
}
|
|
118
|
+
if (request.method === 'GET' && cleanPath === '/sign-out') {
|
|
119
|
+
// Account-shell sign-out is not an OAuth decision. The virtual account has no retained
|
|
120
|
+
// browser cookie, so return to the signed-out entry view and leave the device grant pending.
|
|
121
|
+
return Response.redirect(`${twinPublicBase(request)}/oauth2/device`, 303);
|
|
122
|
+
}
|
|
123
|
+
if (request.method === 'GET' && cleanPath === '/oauth2/device') {
|
|
124
|
+
const invalid = url.searchParams.get('error') === 'invalid_code';
|
|
125
|
+
const userCode = invalid ? '' : url.searchParams.get('user_code') ?? '';
|
|
126
|
+
const view = xaiDeviceAuthView(userCode, options.root, invalid ? 'Invalid or expired code. Please try again.' : undefined, worldNow());
|
|
127
|
+
if (view.status === 'approved' || view.status === 'connected')
|
|
128
|
+
return html(deviceDonePageHtml(true, twinPublicBase(request)));
|
|
129
|
+
if (view.status === 'denied')
|
|
130
|
+
return html(deviceDonePageHtml(false, twinPublicBase(request)));
|
|
131
|
+
return html(deviceEntryPageHtml(view, twinPublicBase(request)));
|
|
132
|
+
}
|
|
133
|
+
if (request.method === 'GET' && cleanPath === '/oauth2/device/consent') {
|
|
134
|
+
const view = xaiDeviceAuthView(url.searchParams.get('user_code') ?? '', options.root, undefined, worldNow());
|
|
135
|
+
if (view.status === 'pending')
|
|
136
|
+
return html(deviceConsentPageHtml(view, twinPublicBase(request)));
|
|
137
|
+
if (view.status === 'approved' || view.status === 'connected')
|
|
138
|
+
return html(deviceDonePageHtml(true, twinPublicBase(request)));
|
|
139
|
+
if (view.status === 'denied')
|
|
140
|
+
return html(deviceDonePageHtml(false, twinPublicBase(request)));
|
|
141
|
+
const message = view.status === 'expired' ? 'This device code has expired. Start sign-in again from your terminal.' : 'Enter a valid device code.';
|
|
142
|
+
return html(deviceEntryPageHtml({ ...view, error: message }, twinPublicBase(request)), view.status === 'unknown' ? 404 : 400);
|
|
143
|
+
}
|
|
144
|
+
if (request.method === 'GET' && cleanPath === '/oauth2/device/done') {
|
|
145
|
+
const view = xaiDeviceAuthView(url.searchParams.get('user_code') ?? '', options.root, undefined, worldNow());
|
|
146
|
+
if (view.status === 'approved' || view.status === 'connected')
|
|
147
|
+
return html(deviceDonePageHtml(true, twinPublicBase(request)));
|
|
148
|
+
if (view.status === 'denied')
|
|
149
|
+
return html(deviceDonePageHtml(false, twinPublicBase(request)));
|
|
150
|
+
const message = view.status === 'expired' ? 'This device code has expired. Start sign-in again from your terminal.' : 'This device authorization has not completed.';
|
|
151
|
+
return html(deviceEntryPageHtml({ ...view, error: message }, twinPublicBase(request)), view.status === 'unknown' ? 404 : 409);
|
|
152
|
+
}
|
|
153
|
+
// Pass through the headers the handler models (auth 401/403, the rate-limit trigger),
|
|
154
|
+
// lower-cased. Their presence is what makes the live wire auth/rate-limit-aware
|
|
155
|
+
// (in-process trusted calls omit them and are not gated).
|
|
156
|
+
const passHeaders = {};
|
|
157
|
+
for (const k of ['authorization', 'x-grok-model-override', 'x-twin-force-rate-limit', 'x-xai-token-auth']) {
|
|
158
|
+
const v = request.headers.get(k);
|
|
159
|
+
if (v !== null)
|
|
160
|
+
passHeaders[k] = v;
|
|
161
|
+
}
|
|
162
|
+
let body = '';
|
|
163
|
+
if (request.method !== 'GET')
|
|
164
|
+
body = await request.text();
|
|
165
|
+
if (request.method === 'POST' && cleanPath === '/oauth2/device/continue') {
|
|
166
|
+
const form = new URLSearchParams(body);
|
|
167
|
+
const view = xaiDeviceAuthView(form.get('user_code') ?? '', options.root, undefined, worldNow());
|
|
168
|
+
if (view.status !== 'pending') {
|
|
169
|
+
return Response.redirect(`${twinPublicBase(request)}/oauth2/device?error=invalid_code`, 303);
|
|
170
|
+
}
|
|
171
|
+
return Response.redirect(`${twinPublicBase(request)}/oauth2/device/consent?user_code=${encodeURIComponent(view.userCode)}`, 303);
|
|
172
|
+
}
|
|
173
|
+
if (request.method === 'POST' && cleanPath === '/oauth2/device/decision') {
|
|
174
|
+
if (readOnly)
|
|
175
|
+
return new Response(JSON.stringify({ code: 'method_not_allowed', error: 'Twin is read-only' }), { status: 405, headers: { 'content-type': 'application/json' } });
|
|
176
|
+
const form = new URLSearchParams(body);
|
|
177
|
+
const approved = form.get('decision') === 'approve';
|
|
178
|
+
const decision = await decideXaiTwinDeviceAuthorization({
|
|
179
|
+
approved,
|
|
180
|
+
occurredAt: worldNow(),
|
|
181
|
+
...(options.root !== undefined ? { root: options.root } : {}),
|
|
182
|
+
userCode: form.get('user_code') ?? '',
|
|
183
|
+
});
|
|
184
|
+
if (decision.status >= 400)
|
|
185
|
+
return new Response(JSON.stringify(decision.body), { status: decision.status, headers: { 'content-type': 'application/json' } });
|
|
186
|
+
return Response.redirect(`${twinPublicBase(request)}/oauth2/device/done?user_code=${encodeURIComponent(String(decision.body.user_code ?? ''))}`, 303);
|
|
187
|
+
}
|
|
188
|
+
// Streaming POST → a real text/event-stream response built from the sink. The handler is
|
|
189
|
+
// synchronous-fast, so events are collected FIRST: a pre-stream failure (validation 400,
|
|
190
|
+
// auth 401/403, rate-limit 429) then returns the real vendor-shaped JSON error with its
|
|
191
|
+
// real status — the vendor rejects a bad request BEFORE opening the event stream, it does
|
|
192
|
+
// not wrap the error in a 200 SSE frame (§9 hardening).
|
|
193
|
+
if (!readOnly && request.method.toUpperCase() === 'POST' && STREAMABLE.has(cleanPath) && wantsStream(body)) {
|
|
194
|
+
const events = [];
|
|
195
|
+
const { status, body: out, headers: errHeaders } = await handleXaiTwinRequest({
|
|
196
|
+
method: request.method, path, body, readOnly, occurredAt: worldNow(), headers: passHeaders, origin: twinPublicBase(request),
|
|
197
|
+
requireTwinOauth: process.env.TWIN_XAI_AUTH_SEAM !== 'unsealed',
|
|
198
|
+
...(options.root !== undefined ? { root: options.root } : {}),
|
|
199
|
+
...(scenarioEngine ? { scenarioEngine } : {}),
|
|
200
|
+
sseSink: (e) => events.push(e),
|
|
201
|
+
});
|
|
202
|
+
if (status >= 400) {
|
|
203
|
+
return new Response(JSON.stringify(out), { status, headers: { 'content-type': 'application/json', 'x-request-id': 'req_twin', ...(errHeaders ?? {}) } });
|
|
204
|
+
}
|
|
205
|
+
const stream = new ReadableStream({
|
|
206
|
+
start(controller) {
|
|
207
|
+
const enc = new TextEncoder();
|
|
208
|
+
for (const e of events)
|
|
209
|
+
controller.enqueue(enc.encode(encodeSse(e)));
|
|
210
|
+
controller.close();
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
return new Response(stream, { headers: { 'content-type': 'text/event-stream; charset=utf-8', 'cache-control': 'no-cache', 'x-request-id': 'req_twin' } });
|
|
214
|
+
}
|
|
215
|
+
const { status, body: out, headers: outHeaders } = await handleXaiTwinRequest({
|
|
216
|
+
method: request.method, path, body, readOnly,
|
|
217
|
+
occurredAt: worldNow(), headers: passHeaders, origin: twinPublicBase(request),
|
|
218
|
+
requireTwinOauth: process.env.TWIN_XAI_AUTH_SEAM !== 'unsealed',
|
|
219
|
+
...(options.root !== undefined ? { root: options.root } : {}),
|
|
220
|
+
...(scenarioEngine ? { scenarioEngine } : {}),
|
|
221
|
+
});
|
|
222
|
+
return new Response(JSON.stringify(out), { status, headers: { 'content-type': 'application/json', 'x-request-id': 'req_twin', ...(outHeaders ?? {}) } });
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
export async function createXaiTwinServer(options) {
|
|
226
|
+
const server = await serveHttp({
|
|
227
|
+
port: options.port ?? 0,
|
|
228
|
+
idleTimeout: 60,
|
|
229
|
+
fetch: createXaiTwinFetch(options),
|
|
230
|
+
});
|
|
231
|
+
return { port: server.port ?? options.port ?? 0, stop: () => server.stop(true) };
|
|
232
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { ChatMessageParam, ChatToolCall } from './xai-types.js';
|
|
2
|
+
/** Deterministic token estimate for a string: ~1 token per 4 chars (faithful order of
|
|
3
|
+
* magnitude; deterministic so usage counts are assertable, like the vendor's tokenizer on a
|
|
4
|
+
* fixed input). Never zero for non-empty text. */
|
|
5
|
+
export declare function estimateTokens(text: string): number;
|
|
6
|
+
/** Flatten a chat message's content (string OR content-part array) to its text for token
|
|
7
|
+
* counting / echo. Non-text parts contribute their JSON length so the count is deterministic
|
|
8
|
+
* and reflects payload size. */
|
|
9
|
+
export declare function contentToText(content: ChatMessageParam['content']): string;
|
|
10
|
+
/** Deterministic per-modality prompt token counts (xAI's usage.prompt_tokens_details splits
|
|
11
|
+
* text vs image tokens; the twin counts image_url parts at a fixed deterministic weight). */
|
|
12
|
+
export declare function countPromptTokenDetails(messages: ChatMessageParam[]): {
|
|
13
|
+
text: number;
|
|
14
|
+
image: number;
|
|
15
|
+
};
|
|
16
|
+
/** Deterministic total prompt-token count for the full set of messages. */
|
|
17
|
+
export declare function countPromptTokens(messages: ChatMessageParam[]): number;
|
|
18
|
+
/** The last user turn's text — the thing the stub echoes (deterministic, clearly labeled). */
|
|
19
|
+
export declare function lastUserText(messages: ChatMessageParam[]): string;
|
|
20
|
+
/**
|
|
21
|
+
* Build the deterministic stub ASSISTANT text. It is unmistakably a twin stub: it carries the
|
|
22
|
+
* `[twin-stub:<model>]` marker and echoes the prompt, so no caller can mistake it for real
|
|
23
|
+
* Grok output. Deterministic for a given prompt → assertable in tests.
|
|
24
|
+
*/
|
|
25
|
+
export declare function stubAssistantText(messages: ChatMessageParam[], model: string): string;
|
|
26
|
+
/** The labeled-stub reasoning trace (grok-3-mini exposes reasoning_content; the twin's is
|
|
27
|
+
* clearly not a real chain-of-thought). */
|
|
28
|
+
export declare function stubReasoningText(messages: ChatMessageParam[], model: string, effort: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* Build a deterministic stub argument string for a tool. When the tool declares a JSON-schema
|
|
31
|
+
* `parameters` object, real Grok emits arguments that validate against the schema; the twin
|
|
32
|
+
* synthesizes a deterministic object containing every declared property with a
|
|
33
|
+
* type-appropriate placeholder so strict callers parse it cleanly.
|
|
34
|
+
*/
|
|
35
|
+
export declare function stubToolArguments(tool: unknown): string;
|
|
36
|
+
/**
|
|
37
|
+
* When tools are provided, real Grok may respond with `tool_calls` and
|
|
38
|
+
* `finish_reason:'tool_calls'`. The stub deterministically "calls" the tool selected by
|
|
39
|
+
* `forcedName` (a named tool_choice) or the FIRST provided tool. Arguments are synthesized
|
|
40
|
+
* from the tool's JSON schema — clearly a stub, but a vendor-faithful tool_calls envelope.
|
|
41
|
+
*/
|
|
42
|
+
export declare function stubToolCall(tools: unknown, seq: number, forcedName?: string): ChatToolCall | null;
|
|
43
|
+
/**
|
|
44
|
+
* Build a deterministic JSON-object stub for `response_format` json_object / json_schema
|
|
45
|
+
* (xAI structured outputs). Always valid JSON; for json_schema every declared property is
|
|
46
|
+
* filled with a schema-typed placeholder so the caller's strict parse succeeds.
|
|
47
|
+
*/
|
|
48
|
+
export declare function stubJsonObject(messages: ChatMessageParam[], model: string, jsonSchema?: unknown): string;
|
|
49
|
+
/** A small deterministic 32-bit hash (FNV-1a) of a string. */
|
|
50
|
+
export declare function fnv1a(text: string): number;
|
|
51
|
+
/** xAI response ids are UUID-formatted strings. Build a deterministic UUID-shaped id from a
|
|
52
|
+
* seed string (stable + assertable, like the twin's other deterministic outputs). */
|
|
53
|
+
export declare function uuidFromSeed(seed: string): string;
|
|
54
|
+
export type XaiToken = {
|
|
55
|
+
token_id: number;
|
|
56
|
+
string_token: string;
|
|
57
|
+
token_bytes: number[];
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Deterministically tokenize text into the faithful /v1/tokenize-text shape. NOT the real
|
|
61
|
+
* Grok BPE — a whitespace-preserving split whose `string_token`s re-join into the exact
|
|
62
|
+
* input, with deterministic token ids seeded from each piece. Shape + determinism are
|
|
63
|
+
* faithful; the ids carry no model meaning.
|
|
64
|
+
*/
|
|
65
|
+
export declare function pseudoTokenize(text: string): XaiToken[];
|
|
66
|
+
/** Deterministic stub citation URLs for a Live Search request: seeded from the query + the
|
|
67
|
+
* requested source types, on a clearly-non-real host; the envelope (citations[] +
|
|
68
|
+
* usage.num_sources_used) is faithful. */
|
|
69
|
+
export declare function stubCitations(query: string, sourceTypes: string[]): string[];
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// THE GENERATIVE-STUB CORE (this twin's answer for generated text).
|
|
2
|
+
//
|
|
3
|
+
// The twin CANNOT run Grok — there are no weights here. So POST /v1/chat/completions,
|
|
4
|
+
// /v1/completions, and /v1/messages return a DETERMINISTIC STUB completion that is CLEARLY a
|
|
5
|
+
// twin stub, NEVER pretending to be real model output; Live Search returns deterministic
|
|
6
|
+
// labeled citation URLs, never real web/X retrieval. What IS faithful is the ENTIRE PROTOCOL
|
|
7
|
+
// ENVELOPE: response shapes, streaming SSE chunk sequences, tool_calls, finish_reason,
|
|
8
|
+
// reasoning_content/reasoning-token accounting, and deterministic usage.
|
|
9
|
+
//
|
|
10
|
+
// The protocol is real; the generation is a deterministic labeled stub.
|
|
11
|
+
/** Deterministic token estimate for a string: ~1 token per 4 chars (faithful order of
|
|
12
|
+
* magnitude; deterministic so usage counts are assertable, like the vendor's tokenizer on a
|
|
13
|
+
* fixed input). Never zero for non-empty text. */
|
|
14
|
+
export function estimateTokens(text) {
|
|
15
|
+
if (!text)
|
|
16
|
+
return 0;
|
|
17
|
+
return Math.max(1, Math.ceil(text.length / 4));
|
|
18
|
+
}
|
|
19
|
+
/** Flatten a chat message's content (string OR content-part array) to its text for token
|
|
20
|
+
* counting / echo. Non-text parts contribute their JSON length so the count is deterministic
|
|
21
|
+
* and reflects payload size. */
|
|
22
|
+
export function contentToText(content) {
|
|
23
|
+
if (typeof content === 'string')
|
|
24
|
+
return content;
|
|
25
|
+
if (content === null || content === undefined)
|
|
26
|
+
return '';
|
|
27
|
+
if (!Array.isArray(content))
|
|
28
|
+
return '';
|
|
29
|
+
return content
|
|
30
|
+
.map((part) => {
|
|
31
|
+
if (part && typeof part === 'object' && part.type === 'text') {
|
|
32
|
+
return String(part.text ?? '');
|
|
33
|
+
}
|
|
34
|
+
return JSON.stringify(part);
|
|
35
|
+
})
|
|
36
|
+
.join('\n');
|
|
37
|
+
}
|
|
38
|
+
/** Deterministic per-modality prompt token counts (xAI's usage.prompt_tokens_details splits
|
|
39
|
+
* text vs image tokens; the twin counts image_url parts at a fixed deterministic weight). */
|
|
40
|
+
export function countPromptTokenDetails(messages) {
|
|
41
|
+
let text = 0;
|
|
42
|
+
let image = 0;
|
|
43
|
+
for (const m of messages) {
|
|
44
|
+
if (Array.isArray(m.content)) {
|
|
45
|
+
for (const part of m.content) {
|
|
46
|
+
const p = part;
|
|
47
|
+
if (p?.type === 'image_url')
|
|
48
|
+
image += 256; // fixed deterministic per-image weight
|
|
49
|
+
else if (p?.type === 'text')
|
|
50
|
+
text += estimateTokens(String(p.text ?? ''));
|
|
51
|
+
else
|
|
52
|
+
text += estimateTokens(JSON.stringify(part));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
text += estimateTokens(contentToText(m.content));
|
|
57
|
+
}
|
|
58
|
+
if (m.name)
|
|
59
|
+
text += estimateTokens(m.name);
|
|
60
|
+
for (const tc of m.tool_calls ?? [])
|
|
61
|
+
text += estimateTokens(JSON.stringify(tc));
|
|
62
|
+
}
|
|
63
|
+
return { text, image };
|
|
64
|
+
}
|
|
65
|
+
/** Deterministic total prompt-token count for the full set of messages. */
|
|
66
|
+
export function countPromptTokens(messages) {
|
|
67
|
+
const d = countPromptTokenDetails(messages);
|
|
68
|
+
return d.text + d.image;
|
|
69
|
+
}
|
|
70
|
+
/** The last user turn's text — the thing the stub echoes (deterministic, clearly labeled). */
|
|
71
|
+
export function lastUserText(messages) {
|
|
72
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
73
|
+
if (messages[i].role === 'user')
|
|
74
|
+
return contentToText(messages[i].content);
|
|
75
|
+
}
|
|
76
|
+
// No user turn (e.g. only system) → fall back to the last message's text.
|
|
77
|
+
return messages.length ? contentToText(messages[messages.length - 1].content) : '';
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Build the deterministic stub ASSISTANT text. It is unmistakably a twin stub: it carries the
|
|
81
|
+
* `[twin-stub:<model>]` marker and echoes the prompt, so no caller can mistake it for real
|
|
82
|
+
* Grok output. Deterministic for a given prompt → assertable in tests.
|
|
83
|
+
*/
|
|
84
|
+
export function stubAssistantText(messages, model) {
|
|
85
|
+
const prompt = lastUserText(messages).trim();
|
|
86
|
+
const echo = prompt.length > 200 ? `${prompt.slice(0, 200)}…` : prompt;
|
|
87
|
+
return `[twin-stub:${model}] This is a deterministic stub from the xAI twin (no model weights are run). Echoing your last message: ${echo || '(empty)'}`;
|
|
88
|
+
}
|
|
89
|
+
/** The labeled-stub reasoning trace (grok-3-mini exposes reasoning_content; the twin's is
|
|
90
|
+
* clearly not a real chain-of-thought). */
|
|
91
|
+
export function stubReasoningText(messages, model, effort) {
|
|
92
|
+
const prompt = lastUserText(messages).trim().slice(0, 80);
|
|
93
|
+
return `[twin-stub:${model}] deterministic reasoning trace (effort=${effort}); no real chain-of-thought is produced. Considering: ${prompt || '(empty)'}`;
|
|
94
|
+
}
|
|
95
|
+
/** Extract a tool's function name from a Chat Completions tool ({ type:'function',
|
|
96
|
+
* function:{ name } }) or a bare { name } entry. */
|
|
97
|
+
function toolName(t) {
|
|
98
|
+
const o = t;
|
|
99
|
+
if (o?.function && typeof o.function.name === 'string')
|
|
100
|
+
return o.function.name;
|
|
101
|
+
if (typeof o?.name === 'string')
|
|
102
|
+
return o.name;
|
|
103
|
+
return 'unknown_function';
|
|
104
|
+
}
|
|
105
|
+
function placeholderForSchema(def) {
|
|
106
|
+
const d = def;
|
|
107
|
+
if (Array.isArray(d?.enum) && d.enum.length)
|
|
108
|
+
return d.enum[0];
|
|
109
|
+
switch (d?.type) {
|
|
110
|
+
case 'number':
|
|
111
|
+
case 'integer': return 0;
|
|
112
|
+
case 'boolean': return false;
|
|
113
|
+
case 'array': return [];
|
|
114
|
+
case 'object': return {};
|
|
115
|
+
default: return '';
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Build a deterministic stub argument string for a tool. When the tool declares a JSON-schema
|
|
120
|
+
* `parameters` object, real Grok emits arguments that validate against the schema; the twin
|
|
121
|
+
* synthesizes a deterministic object containing every declared property with a
|
|
122
|
+
* type-appropriate placeholder so strict callers parse it cleanly.
|
|
123
|
+
*/
|
|
124
|
+
export function stubToolArguments(tool) {
|
|
125
|
+
const o = tool;
|
|
126
|
+
const schema = (o?.function?.parameters ?? o?.parameters ?? o?.input_schema);
|
|
127
|
+
const props = schema && typeof schema === 'object' ? schema.properties : undefined;
|
|
128
|
+
if (!props || typeof props !== 'object')
|
|
129
|
+
return '{}';
|
|
130
|
+
const out = {};
|
|
131
|
+
for (const [key, def] of Object.entries(props))
|
|
132
|
+
out[key] = placeholderForSchema(def);
|
|
133
|
+
return JSON.stringify(out);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* When tools are provided, real Grok may respond with `tool_calls` and
|
|
137
|
+
* `finish_reason:'tool_calls'`. The stub deterministically "calls" the tool selected by
|
|
138
|
+
* `forcedName` (a named tool_choice) or the FIRST provided tool. Arguments are synthesized
|
|
139
|
+
* from the tool's JSON schema — clearly a stub, but a vendor-faithful tool_calls envelope.
|
|
140
|
+
*/
|
|
141
|
+
export function stubToolCall(tools, seq, forcedName) {
|
|
142
|
+
if (!Array.isArray(tools) || tools.length === 0)
|
|
143
|
+
return null;
|
|
144
|
+
const chosen = forcedName ? (tools.find((t) => toolName(t) === forcedName) ?? tools[0]) : tools[0];
|
|
145
|
+
return { id: `call_twin_${seq}`, type: 'function', function: { name: toolName(chosen), arguments: stubToolArguments(chosen) } };
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Build a deterministic JSON-object stub for `response_format` json_object / json_schema
|
|
149
|
+
* (xAI structured outputs). Always valid JSON; for json_schema every declared property is
|
|
150
|
+
* filled with a schema-typed placeholder so the caller's strict parse succeeds.
|
|
151
|
+
*/
|
|
152
|
+
export function stubJsonObject(messages, model, jsonSchema) {
|
|
153
|
+
const schema = jsonSchema;
|
|
154
|
+
const props = schema?.schema?.properties ?? schema?.properties;
|
|
155
|
+
if (props && typeof props === 'object') {
|
|
156
|
+
const out = {};
|
|
157
|
+
for (const [key, def] of Object.entries(props))
|
|
158
|
+
out[key] = placeholderForSchema(def);
|
|
159
|
+
return JSON.stringify(out);
|
|
160
|
+
}
|
|
161
|
+
return JSON.stringify({ _twin_stub: true, model, echo: lastUserText(messages).slice(0, 200) });
|
|
162
|
+
}
|
|
163
|
+
// ── Deterministic hashing / ids ─────────────────────────────────────────────────────────
|
|
164
|
+
/** A small deterministic 32-bit hash (FNV-1a) of a string. */
|
|
165
|
+
export function fnv1a(text) {
|
|
166
|
+
let h = 0x811c9dc5;
|
|
167
|
+
for (let i = 0; i < text.length; i++) {
|
|
168
|
+
h ^= text.charCodeAt(i);
|
|
169
|
+
h = Math.imul(h, 0x01000193);
|
|
170
|
+
}
|
|
171
|
+
return h >>> 0;
|
|
172
|
+
}
|
|
173
|
+
/** xAI response ids are UUID-formatted strings. Build a deterministic UUID-shaped id from a
|
|
174
|
+
* seed string (stable + assertable, like the twin's other deterministic outputs). */
|
|
175
|
+
export function uuidFromSeed(seed) {
|
|
176
|
+
const h = (s) => fnv1a(s).toString(16).padStart(8, '0');
|
|
177
|
+
const a = h(seed);
|
|
178
|
+
const b = h(`${seed}#1`);
|
|
179
|
+
const c = h(`${seed}#2`);
|
|
180
|
+
const d = h(`${seed}#3`);
|
|
181
|
+
return `${a}-${b.slice(0, 4)}-${b.slice(4)}-${c.slice(0, 4)}-${c.slice(4)}${d.slice(0, 8)}`;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Deterministically tokenize text into the faithful /v1/tokenize-text shape. NOT the real
|
|
185
|
+
* Grok BPE — a whitespace-preserving split whose `string_token`s re-join into the exact
|
|
186
|
+
* input, with deterministic token ids seeded from each piece. Shape + determinism are
|
|
187
|
+
* faithful; the ids carry no model meaning.
|
|
188
|
+
*/
|
|
189
|
+
export function pseudoTokenize(text) {
|
|
190
|
+
const pieces = text.match(/\s+|\S+/g) ?? [];
|
|
191
|
+
return pieces.map((piece) => ({
|
|
192
|
+
token_id: fnv1a(piece) % 200000,
|
|
193
|
+
string_token: piece,
|
|
194
|
+
token_bytes: Array.from(new TextEncoder().encode(piece)),
|
|
195
|
+
}));
|
|
196
|
+
}
|
|
197
|
+
// ── Live Search: deterministic labeled citations ────────────────────────────────────────
|
|
198
|
+
/** Deterministic stub citation URLs for a Live Search request: seeded from the query + the
|
|
199
|
+
* requested source types, on a clearly-non-real host; the envelope (citations[] +
|
|
200
|
+
* usage.num_sources_used) is faithful. */
|
|
201
|
+
export function stubCitations(query, sourceTypes) {
|
|
202
|
+
const types = sourceTypes.length ? sourceTypes : ['web'];
|
|
203
|
+
const out = [];
|
|
204
|
+
for (const t of types) {
|
|
205
|
+
const seed = fnv1a(`${t}|${query}`).toString(36);
|
|
206
|
+
out.push(`https://twin.invalid/xai-live-search-stub/${t}/${seed}-1`);
|
|
207
|
+
out.push(`https://twin.invalid/xai-live-search-stub/${t}/${seed}-2`);
|
|
208
|
+
}
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { type XaiScenarioEngine } from './xai-scenario.js';
|
|
2
|
+
import type { ChatCompletion, ChatMessageParam, SseSink } from './xai-types.js';
|
|
3
|
+
export type XaiRequest = {
|
|
4
|
+
method: string;
|
|
5
|
+
path: string;
|
|
6
|
+
body?: string;
|
|
7
|
+
occurredAt?: string;
|
|
8
|
+
root?: string;
|
|
9
|
+
readOnly?: boolean;
|
|
10
|
+
/** Where the HTTP twin serving this request is reached (`twinPublicBase`: origin plus any
|
|
11
|
+
* served-World mount path). OAuth device responses use it for their verification URLs. In-process calls may omit it because they do not start OAuth. */
|
|
12
|
+
origin?: string;
|
|
13
|
+
/** A sealed World requires Grok CLI bearer tokens to have been issued by this same Twin's
|
|
14
|
+
* OAuth state. Compatibility/local mode can leave this false to combine real OAuth with
|
|
15
|
+
* twinned inference as an explicitly unsealed identity seam. */
|
|
16
|
+
requireTwinOauth?: boolean;
|
|
17
|
+
/** The credential the caller presents (the SDK's bearer `Authorization` header). When a request
|
|
18
|
+
* carries an auth SURFACE (this field set, or `headers` present), the twin holds it to the real
|
|
19
|
+
* vendor rule: a credential is required → 401 on missing/invalid, 403 on a blocked key.
|
|
20
|
+
* In-process trusted calls (capability verify, connector) omit BOTH and are not auth-gated —
|
|
21
|
+
* the twin can't validate against real keys, so the modeled failure is the CHECKABLE
|
|
22
|
+
* missing/sentinel case ('xai-invalid' → 401, 'xai-blocked' → 403). */
|
|
23
|
+
apiKey?: string;
|
|
24
|
+
/** Lower-cased request headers the HTTP server passes through so the handler can model auth
|
|
25
|
+
* (401/403) and the deterministic rate-limit trigger (429). */
|
|
26
|
+
headers?: Record<string, string>;
|
|
27
|
+
/** When set on a streaming POST, chunks are written here (no sockets). */
|
|
28
|
+
sseSink?: SseSink;
|
|
29
|
+
/** Optional scenario-scripting session (xai-scenario.ts): when set, POST /v1/chat/completions
|
|
30
|
+
* first consults the scenario's ordered rules and serves a scripted completion on a match
|
|
31
|
+
* (falling back to the normal deterministic stub otherwise). Twin scaffolding, not vendor
|
|
32
|
+
* surface — deliberately absent from the capability manifest. */
|
|
33
|
+
scenarioEngine?: XaiScenarioEngine;
|
|
34
|
+
};
|
|
35
|
+
/** The handler response. `headers` (when present) are response headers the HTTP server should
|
|
36
|
+
* set — e.g. `retry-after` on a modeled 429. */
|
|
37
|
+
export type XaiResponseEnvelope = {
|
|
38
|
+
status: number;
|
|
39
|
+
body: unknown;
|
|
40
|
+
headers?: Record<string, string>;
|
|
41
|
+
};
|
|
42
|
+
type SearchArgs = {
|
|
43
|
+
mode: 'auto' | 'on' | 'off';
|
|
44
|
+
sourceTypes: string[];
|
|
45
|
+
returnCitations: boolean;
|
|
46
|
+
};
|
|
47
|
+
type ToolChoice = 'auto' | 'none' | 'required' | {
|
|
48
|
+
name: string;
|
|
49
|
+
};
|
|
50
|
+
type ResponseFormat = {
|
|
51
|
+
kind: 'text';
|
|
52
|
+
} | {
|
|
53
|
+
kind: 'json_object';
|
|
54
|
+
} | {
|
|
55
|
+
kind: 'json_schema';
|
|
56
|
+
schema: unknown;
|
|
57
|
+
};
|
|
58
|
+
type ChatArgs = {
|
|
59
|
+
model: string;
|
|
60
|
+
requestedModel: string;
|
|
61
|
+
messages: ChatMessageParam[];
|
|
62
|
+
tools?: unknown;
|
|
63
|
+
n: number;
|
|
64
|
+
maxTokens?: number;
|
|
65
|
+
stop?: string[];
|
|
66
|
+
stream: boolean;
|
|
67
|
+
toolChoice?: ToolChoice;
|
|
68
|
+
parallelToolCalls: boolean;
|
|
69
|
+
responseFormat: ResponseFormat;
|
|
70
|
+
includeUsage: boolean;
|
|
71
|
+
seed?: number;
|
|
72
|
+
/** reasoning_effort ('low'|'high') — grok-3-mini ONLY; grok-4-family rejects it (vendor 400). */
|
|
73
|
+
reasoningEffort?: string;
|
|
74
|
+
/** Live Search (xAI delta). */
|
|
75
|
+
search?: SearchArgs;
|
|
76
|
+
/** deferred:true → respond with { request_id } and serve the result on poll. */
|
|
77
|
+
deferred: boolean;
|
|
78
|
+
};
|
|
79
|
+
export declare function buildChatCompletion(args: ChatArgs, occurredAt?: string, scenarioEngine?: XaiScenarioEngine): ChatCompletion;
|
|
80
|
+
/**
|
|
81
|
+
* Emit the vendor-faithful chat streaming sequence into the injected sink (NO sockets, NO
|
|
82
|
+
* setTimeout). Real order: a first chunk with `delta:{role:'assistant'}`, then
|
|
83
|
+
* `delta:{content}` chunks (or tool_calls deltas), then a final chunk with `finish_reason`,
|
|
84
|
+
* then a usage-only chunk when stream_options.include_usage, then `[DONE]`. Deterministic +
|
|
85
|
+
* synchronous so a collector can assert the full sequence.
|
|
86
|
+
*/
|
|
87
|
+
export declare function streamChat(args: ChatArgs, sink: SseSink, occurredAt?: string, scenarioEngine?: XaiScenarioEngine): ChatCompletion;
|
|
88
|
+
export declare function handleXaiTwinRequest(req: XaiRequest): Promise<XaiResponseEnvelope>;
|
|
89
|
+
export {};
|