@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.
Files changed (56) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +246 -0
  3. package/client/xai-device-auth.css +246 -0
  4. package/client/xai-device-auth.tsx +138 -0
  5. package/dist/client/xai-device-auth.bundle.js +18 -0
  6. package/dist/client/xai-device-auth.css +246 -0
  7. package/dist/client/xai-device-auth.d.ts +19 -0
  8. package/dist/client/xai-device-auth.js +50 -0
  9. package/dist/client/xai-device-auth.tsx +138 -0
  10. package/dist/src/cli.d.ts +2 -0
  11. package/dist/src/cli.js +28 -0
  12. package/dist/src/index.d.ts +17 -0
  13. package/dist/src/index.js +70 -0
  14. package/dist/src/xai-budget.d.ts +60 -0
  15. package/dist/src/xai-budget.js +139 -0
  16. package/dist/src/xai-capabilities.d.ts +4 -0
  17. package/dist/src/xai-capabilities.js +1072 -0
  18. package/dist/src/xai-conformance.d.ts +13 -0
  19. package/dist/src/xai-conformance.js +148 -0
  20. package/dist/src/xai-connector.d.ts +82 -0
  21. package/dist/src/xai-connector.js +174 -0
  22. package/dist/src/xai-device-auth-css.gen.d.ts +1 -0
  23. package/dist/src/xai-device-auth-css.gen.js +6 -0
  24. package/dist/src/xai-device-auth-ui.d.ts +13 -0
  25. package/dist/src/xai-device-auth-ui.js +72 -0
  26. package/dist/src/xai-models.d.ts +57 -0
  27. package/dist/src/xai-models.js +102 -0
  28. package/dist/src/xai-oauth.d.ts +30 -0
  29. package/dist/src/xai-oauth.js +279 -0
  30. package/dist/src/xai-scenario.d.ts +33 -0
  31. package/dist/src/xai-scenario.js +139 -0
  32. package/dist/src/xai-server.d.ts +36 -0
  33. package/dist/src/xai-server.js +232 -0
  34. package/dist/src/xai-stub.d.ts +69 -0
  35. package/dist/src/xai-stub.js +210 -0
  36. package/dist/src/xai-twin.d.ts +89 -0
  37. package/dist/src/xai-twin.js +883 -0
  38. package/dist/src/xai-types.d.ts +118 -0
  39. package/dist/src/xai-types.js +6 -0
  40. package/package.json +76 -0
  41. package/src/cli.ts +27 -0
  42. package/src/index.ts +120 -0
  43. package/src/xai-budget.ts +165 -0
  44. package/src/xai-capabilities.ts +1046 -0
  45. package/src/xai-conformance.ts +136 -0
  46. package/src/xai-connector.ts +212 -0
  47. package/src/xai-device-auth-css.gen.ts +6 -0
  48. package/src/xai-device-auth-ui.ts +90 -0
  49. package/src/xai-journey.uitest.ts +155 -0
  50. package/src/xai-models.ts +154 -0
  51. package/src/xai-oauth.ts +301 -0
  52. package/src/xai-scenario.ts +148 -0
  53. package/src/xai-server.ts +258 -0
  54. package/src/xai-stub.ts +213 -0
  55. package/src/xai-twin.ts +960 -0
  56. 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 {};