@adia-ai/a2ui-compose 0.2.5 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -12,6 +12,63 @@ generator graph.
12
12
 
13
13
  _No pending changes._
14
14
 
15
+ ## [0.3.1] - 2026-05-06
16
+
17
+ **9-package lockstep patch cut.** All 9 published `@adia-ai/*` packages bump 0.3.0 → 0.3.1 per [`docs/specs/package-architecture.md` § 15](../../../docs/specs/package-architecture.md#15-versioning-policy). Internal `@adia-ai/*` dep ranges remain at `^0.3.0` (covers `0.3.1` under semver — patch-cut asymmetry).
18
+
19
+ This package itself ships **no source changes** in v0.3.1. The cut bumps version only — substantive content lives in [`@adia-ai/web-components`](https://github.com/adiahealth/gen-ui-kit/releases/tag/web-components-v0.3.1) (folder-per-trait restructure; barrel API unchanged).
20
+
21
+ ### Changed
22
+
23
+ - `version`: `0.3.0` → `0.3.1`.
24
+ - Internal `@adia-ai/*` dep ranges: unchanged at `^0.3.0` (covers `0.3.1` under semver — patch-cut asymmetry).
25
+
26
+ ## [0.3.0] - 2026-05-05
27
+
28
+ **9-package lockstep cut + LLM subpath dropped.** All 9 published `@adia-ai/*` packages bump 0.2.5 → 0.3.0 per [`docs/specs/package-architecture.md` § 15](../../../docs/specs/package-architecture.md#15-versioning-policy). Internal `@adia-ai/*` dep ranges bump `^0.2.0` → `^0.3.0`.
29
+
30
+ This is a **minor cut on top of v0.2.5 with one BREAKING change**: the `./llm` subpath export is removed. The LLM adapters that previously shipped under `@adia-ai/a2ui-compose/llm` now ship as their own package, `@adia-ai/llm` (9th lockstep member, first publish in this cut).
31
+
32
+ ### Removed
33
+
34
+ - **`./llm` subpath export.** The package no longer re-exports the LLM
35
+ adapters or bridge. Migrate to `@adia-ai/llm`:
36
+
37
+ ```diff
38
+ - import { streamChat } from '@adia-ai/a2ui-compose/llm/adapters/index.js';
39
+ + import { streamChat } from '@adia-ai/llm';
40
+
41
+ - import { createAdapter } from '@adia-ai/a2ui-compose/llm/llm-bridge.js';
42
+ + import { createAdapter } from '@adia-ai/llm/bridge';
43
+ ```
44
+
45
+ See `@adia-ai/llm@0.3.0` CHANGELOG for the full migration narrative.
46
+
47
+ ### Changed
48
+
49
+ - `dependencies["@adia-ai/llm"]`: NEW, `^0.3.0`. The compose engines'
50
+ internal LLM use now resolves through the new package.
51
+ - `dependencies["@adia-ai/a2ui-utils"]` removed (renamed to `a2ui-runtime`); replaced by `dependencies["@adia-ai/a2ui-runtime"]: ^0.3.0`.
52
+ - All other internal `@adia-ai/*` dep ranges: `^0.2.0` → `^0.3.0`.
53
+ - `version`: `0.2.5` → `0.3.0`.
54
+ - Source paths internal to compose that referenced `./llm/` updated to
55
+ `../../../llm/` (e.g. `core/generator.js` imports `StubLLMAdapter`
56
+ from the new package location).
57
+ - `transpiler/transpiler-maps.js`: `@adia-ai/a2ui-utils` import → `@adia-ai/a2ui-runtime` (matches the package rename — see runtime's CHANGELOG).
58
+
59
+ ### Migration
60
+
61
+ ```diff
62
+ {
63
+ "dependencies": {
64
+ "@adia-ai/a2ui-compose": "^0.3.0",
65
+ + "@adia-ai/llm": "^0.3.0"
66
+ }
67
+ }
68
+ ```
69
+
70
+ Plus rewrite imports as shown above. The change is mechanical.
71
+
15
72
  ## [0.2.5] - 2026-05-04
16
73
 
17
74
  **8-package lockstep cut.** All 8 published `@adia-ai/*` packages bump 0.2.4 → 0.2.5 per [`docs/specs/package-architecture.md` § 15](../../../docs/specs/package-architecture.md#15-versioning-policy). Internal `@adia-ai/*` dep ranges remain at `^0.2.0` (covers 0.2.5 under semver — patch-cut asymmetry).
package/README.md CHANGED
@@ -8,7 +8,7 @@ ready for a renderer.
8
8
 
9
9
  > This package is pipeline runtime only. UI components live in
10
10
  > [`@adia-ai/web-components`](../../web-components); the A2UI runtime (renderer,
11
- > registry, streams, wiring) in [`@adia-ai/a2ui-utils`](../utils);
11
+ > registry, streams, wiring) in [`@adia-ai/a2ui-runtime`](../runtime);
12
12
  > the pattern corpus in [`@adia-ai/a2ui-corpus`](../corpus);
13
13
  > the MCP server in [`@adia-ai/a2ui-mcp`](../mcp).
14
14
  >
@@ -140,7 +140,7 @@ output, not a safety net.
140
140
  Multi-provider adapter with a common interface:
141
141
 
142
142
  ```javascript
143
- import { getAdapter } from '@adia-ai/a2ui-compose/llm';
143
+ import { getAdapter } from '@adia-ai/llm';
144
144
 
145
145
  const adapter = getAdapter('anthropic'); // or 'openai', 'gemini'
146
146
  const stream = await adapter.streamChat({ model, messages, tools });
package/core/generator.js CHANGED
@@ -19,8 +19,8 @@ import { decomposeIntent, composeSubtasks } from '../../retrieval/intent/decompo
19
19
  import { getWiringCatalog } from '../../retrieval/wiring-catalog.js';
20
20
  import { getComponentData } from '../../retrieval/pattern-library.js';
21
21
 
22
- import { StubLLMAdapter } from '../llm/llm-stub.js';
23
- import { createAdapter } from '../llm/llm-bridge.js';
22
+ import { StubLLMAdapter } from '../../../llm/llm-stub.js';
23
+ import { createAdapter } from '../../../llm/llm-bridge.js';
24
24
  import { assessClarity } from '../../retrieval/intent/clarity.js';
25
25
  import { isConversational } from '../../retrieval/intent/intent-gate.js';
26
26
  import { classifyIntent } from '../../retrieval/domain-router.js';
package/index.js CHANGED
@@ -7,8 +7,8 @@
7
7
  * import { generateUI } from '@adia-ai/a2ui-compose';
8
8
  * import { pick, registerEngine } from '@adia-ai/a2ui-compose/strategies/registry';
9
9
  * import { generateZettel } from '@adia-ai/a2ui-compose/strategies/zettel';
10
- * import { llmBridge } from '@adia-ai/a2ui-compose/llm';
11
10
  *
11
+ * The LLM bridge moved to its own package — `@adia-ai/llm/bridge`.
12
12
  * See README for the full public surface.
13
13
  */
14
14
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adia-ai/a2ui-compose",
3
- "version": "0.2.5",
3
+ "version": "0.3.1",
4
4
  "description": "AdiaUI A2UI compose engine \u2014 framework-agnostic. Takes natural-language intents + a catalog and produces A2UI protocol messages. Pairs with `@adia-ai/a2ui-retrieval` (intent classification, catalog lookup) and `@adia-ai/a2ui-validator` (schema + semantic checks).",
5
5
  "type": "module",
6
6
  "exports": {
@@ -8,8 +8,6 @@
8
8
  "./core": "./core/generator.js",
9
9
  "./strategies/zettel": "./strategies/zettel/generator-adapter.js",
10
10
  "./strategies/registry": "./strategies/registry.js",
11
- "./llm": "./llm/llm-bridge.js",
12
- "./llm/*": "./llm/*.js",
13
11
  "./transpiler": "./transpiler/transpiler.js",
14
12
  "./evals": "./evals/harness.mjs",
15
13
  "./engine": "./core/generator.js",
@@ -19,7 +17,6 @@
19
17
  "files": [
20
18
  "core/",
21
19
  "strategies/",
22
- "llm/",
23
20
  "evals/",
24
21
  "transpiler/",
25
22
  "index.js",
@@ -37,8 +34,9 @@
37
34
  "directory": "packages/a2ui/compose"
38
35
  },
39
36
  "dependencies": {
40
- "@adia-ai/a2ui-utils": "^0.2.0",
41
- "@adia-ai/a2ui-retrieval": "^0.2.0",
42
- "@adia-ai/a2ui-validator": "^0.2.0"
37
+ "@adia-ai/a2ui-runtime": "^0.3.0",
38
+ "@adia-ai/a2ui-retrieval": "^0.3.0",
39
+ "@adia-ai/a2ui-validator": "^0.3.0",
40
+ "@adia-ai/llm": "^0.3.0"
43
41
  }
44
42
  }
@@ -8,7 +8,7 @@
8
8
  * 3. Input type / ARIA role sub-maps
9
9
  */
10
10
 
11
- import { registry } from '@adia-ai/a2ui-utils';
11
+ import { registry } from '@adia-ai/a2ui-runtime';
12
12
 
13
13
  // ── Alias types to skip in reverse registry (prefer primary type) ────────
14
14
 
@@ -1,106 +0,0 @@
1
- /**
2
- * Anthropic Messages API adapter.
3
- * Endpoint: https://api.anthropic.com/v1/messages
4
- */
5
-
6
- import { readSSE } from './sse.js';
7
-
8
- const API_URL = 'https://api.anthropic.com/v1/messages';
9
- const API_VERSION = '2023-06-01';
10
- const DEFAULT_MAX_TOKENS = 4096;
11
-
12
- export const anthropic = {
13
- name: 'anthropic',
14
-
15
- buildRequest(opts) {
16
- const body = {
17
- model: opts.model,
18
- max_tokens: opts.maxTokens || DEFAULT_MAX_TOKENS,
19
- messages: opts.messages,
20
- stream: !!opts.stream,
21
- };
22
- if (opts.system) {
23
- // Prompt caching: the AdiaUI system prompt is ~23KB and constant across
24
- // a session. Emitting it as a cached block marks it as a cache breakpoint
25
- // (ephemeral, ~5 min TTL). First call = cache write (+25% cost), every
26
- // subsequent call in the window = cache read (−90% cost). No-op below
27
- // the model's minimum cacheable size (1024 tok Sonnet/Opus, 2048 Haiku).
28
- body.system = opts.cache
29
- ? [{ type: 'text', text: opts.system, cache_control: { type: 'ephemeral' } }]
30
- : opts.system;
31
- }
32
- if (opts.temperature != null) body.temperature = opts.temperature;
33
- if (opts.thinking) {
34
- body.thinking = { type: 'enabled', budget_tokens: opts.thinkingBudget || 10000 };
35
- }
36
-
37
- return {
38
- url: opts.proxyUrl || API_URL,
39
- headers: {
40
- 'content-type': 'application/json',
41
- 'x-api-key': opts.apiKey,
42
- 'anthropic-version': API_VERSION,
43
- },
44
- body,
45
- };
46
- },
47
-
48
- parseResponse(data) {
49
- const text = data.content?.find(b => b.type === 'text')?.text ?? '';
50
- return {
51
- text,
52
- usage: {
53
- input: data.usage?.input_tokens ?? 0,
54
- output: data.usage?.output_tokens ?? 0,
55
- // Cache telemetry: non-zero cacheRead on turn 2+ is the signal that
56
- // caching is actually kicking in. Recorded per-turn for hit-rate analysis.
57
- cacheCreation: data.usage?.cache_creation_input_tokens ?? 0,
58
- cacheRead: data.usage?.cache_read_input_tokens ?? 0,
59
- },
60
- stopReason: data.stop_reason ?? 'end',
61
- };
62
- },
63
-
64
- async *parseStream(response) {
65
- let snapshot = '';
66
- let usage = { input: 0, output: 0, cacheCreation: 0, cacheRead: 0 };
67
- let stopReason = 'end';
68
-
69
- for await (const event of readSSE(response.body)) {
70
- if (event.done) break;
71
- let data;
72
- try { data = JSON.parse(event.data); } catch { continue; }
73
- const eventType = event.event ?? data.type;
74
-
75
- switch (eventType) {
76
- case 'message_start':
77
- if (data.message?.usage) {
78
- usage.input = data.message.usage.input_tokens ?? 0;
79
- usage.cacheCreation = data.message.usage.cache_creation_input_tokens ?? 0;
80
- usage.cacheRead = data.message.usage.cache_read_input_tokens ?? 0;
81
- }
82
- break;
83
- case 'content_block_delta': {
84
- const delta = data.delta;
85
- if (delta?.type === 'text_delta') {
86
- snapshot += delta.text;
87
- yield { type: 'text', text: delta.text, snapshot };
88
- } else if (delta?.type === 'thinking_delta') {
89
- yield { type: 'thinking', text: delta.thinking };
90
- }
91
- break;
92
- }
93
- case 'message_delta':
94
- if (data.delta?.stop_reason) stopReason = data.delta.stop_reason;
95
- if (data.usage) usage.output = data.usage.output_tokens ?? 0;
96
- break;
97
- case 'message_stop':
98
- yield { type: 'done', text: snapshot, usage, stopReason };
99
- break;
100
- case 'error':
101
- yield { type: 'error', error: new Error(data.error?.message ?? 'Stream error') };
102
- break;
103
- }
104
- }
105
- },
106
- };
@@ -1,99 +0,0 @@
1
- /**
2
- * Google Gemini generateContent API adapter.
3
- * Endpoint: https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent
4
- * Streaming: .../{model}:streamGenerateContent?alt=sse
5
- */
6
-
7
- import { readSSE } from './sse.js';
8
-
9
- const API_URL = 'https://generativelanguage.googleapis.com/v1beta/models';
10
- const DEFAULT_MAX_TOKENS = 4096;
11
-
12
- export const gemini = {
13
- name: 'gemini',
14
-
15
- buildRequest(opts) {
16
- const model = opts.model;
17
- const contents = [];
18
- for (const msg of opts.messages) {
19
- contents.push({
20
- role: msg.role === 'assistant' ? 'model' : 'user',
21
- parts: [{ text: msg.content }],
22
- });
23
- }
24
-
25
- const body = { contents };
26
-
27
- if (opts.system) {
28
- body.systemInstruction = { parts: [{ text: opts.system }] };
29
- }
30
-
31
- const generationConfig = {
32
- maxOutputTokens: opts.maxTokens || DEFAULT_MAX_TOKENS,
33
- };
34
- if (opts.temperature != null) generationConfig.temperature = opts.temperature;
35
- body.generationConfig = generationConfig;
36
-
37
- const action = opts.stream
38
- ? `streamGenerateContent?alt=sse`
39
- : 'generateContent';
40
-
41
- return {
42
- url: opts.proxyUrl || `${API_URL}/${model}:${action}`,
43
- headers: {
44
- 'content-type': 'application/json',
45
- 'x-goog-api-key': opts.apiKey,
46
- },
47
- body,
48
- };
49
- },
50
-
51
- parseResponse(data) {
52
- const parts = data.candidates?.[0]?.content?.parts ?? [];
53
- const text = parts.map(p => p.text ?? '').join('');
54
- return {
55
- text,
56
- usage: {
57
- input: data.usageMetadata?.promptTokenCount ?? 0,
58
- output: data.usageMetadata?.candidatesTokenCount ?? 0,
59
- },
60
- stopReason: data.candidates?.[0]?.finishReason === 'STOP' ? 'end' : 'end',
61
- };
62
- },
63
-
64
- async *parseStream(response) {
65
- let snapshot = '';
66
- let usage = { input: 0, output: 0 };
67
- let stopReason = 'end';
68
-
69
- for await (const event of readSSE(response.body)) {
70
- if (event.done) break;
71
- let data;
72
- try { data = JSON.parse(event.data); } catch { continue; }
73
-
74
- if (data.usageMetadata) {
75
- usage.input = data.usageMetadata.promptTokenCount ?? 0;
76
- usage.output = data.usageMetadata.candidatesTokenCount ?? 0;
77
- }
78
-
79
- const candidate = data.candidates?.[0];
80
- if (!candidate) continue;
81
-
82
- if (candidate.finishReason && candidate.finishReason !== 'STOP') {
83
- stopReason = candidate.finishReason;
84
- }
85
-
86
- const parts = candidate.content?.parts;
87
- if (!parts?.length) continue;
88
-
89
- for (const part of parts) {
90
- if (part.text != null) {
91
- snapshot += part.text;
92
- yield { type: 'text', text: part.text, snapshot };
93
- }
94
- }
95
- }
96
-
97
- yield { type: 'done', text: snapshot, usage, stopReason };
98
- },
99
- };
@@ -1,138 +0,0 @@
1
- /**
2
- * LLM Client — Provider-agnostic chat interface.
3
- *
4
- * Usage:
5
- * import { createClient, chat, streamChat } from './llm/index.js';
6
- *
7
- * // Quick use (provider auto-detected from model name)
8
- * const reply = await chat({
9
- * apiKey: 'sk-ant-...',
10
- * model: 'claude-sonnet-4-20250514',
11
- * messages: [{ role: 'user', content: 'Hello' }],
12
- * });
13
- *
14
- * for await (const chunk of streamChat({
15
- * apiKey: 'sk-...',
16
- * model: 'gpt-4o',
17
- * messages: [{ role: 'user', content: 'Hello' }],
18
- * })) {
19
- * if (chunk.type === 'text') process.stdout.write(chunk.text);
20
- * }
21
- *
22
- * // Explicit provider
23
- * const reply = await chat({ provider: 'gemini', apiKey: '...', model: 'gemini-2.5-flash', ... });
24
- *
25
- * // Reusable client instance
26
- * const client = createClient({ provider: 'anthropic', apiKey: '...' });
27
- * const reply = await client.chat({ model: 'claude-sonnet-4-20250514', messages: [...] });
28
- * for await (const chunk of client.stream({ model: '...', messages: [...] })) { ... }
29
- *
30
- * Chunk types (streaming):
31
- * { type: 'text', text: 'delta', snapshot: 'full text so far' }
32
- * { type: 'thinking', text: 'thinking delta' }
33
- * { type: 'done', text: 'full response', usage: { input, output }, stopReason }
34
- * { type: 'error', error: Error }
35
- */
36
-
37
- import { anthropic } from './anthropic.js';
38
- import { openai } from './openai.js';
39
- import { gemini } from './gemini.js';
40
-
41
- // ── Provider registry ──
42
-
43
- const providers = { anthropic, openai, gemini };
44
-
45
- /** Detect provider from model name. */
46
- function detectProvider(model) {
47
- if (!model) return null;
48
- const m = model.toLowerCase();
49
- if (m.includes('claude') || m.startsWith('anthropic/')) return 'anthropic';
50
- if (m.includes('gpt') || m.includes('o1') || m.includes('o3') || m.includes('o4') || m.startsWith('openai/')) return 'openai';
51
- if (m.includes('gemini') || m.startsWith('google/')) return 'gemini';
52
- return null;
53
- }
54
-
55
- function resolveAdapter(opts) {
56
- const name = opts.provider || detectProvider(opts.model);
57
- if (!name) throw new Error(`Cannot detect provider for model "${opts.model}". Set provider explicitly.`);
58
- const adapter = providers[name];
59
- if (!adapter) throw new Error(`Unknown provider "${name}". Available: ${Object.keys(providers).join(', ')}`);
60
- return adapter;
61
- }
62
-
63
- // ── Standalone functions ──
64
-
65
- /**
66
- * Non-streaming chat completion.
67
- * @returns {Promise<{text: string, usage: {input: number, output: number}, stopReason: string}>}
68
- */
69
- export async function chat(opts) {
70
- const adapter = resolveAdapter(opts);
71
- const { url, headers, body } = adapter.buildRequest({ ...opts, stream: false });
72
-
73
- const res = await fetch(url, {
74
- method: 'POST',
75
- headers,
76
- body: JSON.stringify(body),
77
- signal: opts.signal,
78
- });
79
-
80
- if (!res.ok) {
81
- const err = await res.json().catch(() => ({}));
82
- throw new Error(err?.error?.message || `${adapter.name} API error ${res.status}`);
83
- }
84
-
85
- return adapter.parseResponse(await res.json());
86
- }
87
-
88
- /**
89
- * Streaming chat — yields chunks as they arrive.
90
- * @returns {AsyncGenerator<{type: string, text?: string, snapshot?: string, usage?: object, error?: Error}>}
91
- */
92
- export async function* streamChat(opts) {
93
- const adapter = resolveAdapter(opts);
94
- const { url, headers, body } = adapter.buildRequest({ ...opts, stream: true });
95
-
96
- let res;
97
- try {
98
- res = await fetch(url, {
99
- method: 'POST',
100
- headers,
101
- body: JSON.stringify(body),
102
- signal: opts.signal,
103
- });
104
- } catch (err) {
105
- yield { type: 'error', error: err };
106
- return;
107
- }
108
-
109
- if (!res.ok) {
110
- const err = await res.json().catch(() => ({}));
111
- yield { type: 'error', error: new Error(err?.error?.message || `${adapter.name} API error ${res.status}`) };
112
- return;
113
- }
114
-
115
- yield* adapter.parseStream(res);
116
- }
117
-
118
- // ── Client factory ──
119
-
120
- /**
121
- * Create a reusable client instance with defaults baked in.
122
- *
123
- * @param {object} defaults
124
- * @param {string} defaults.provider — 'anthropic' | 'openai' | 'gemini'
125
- * @param {string} defaults.apiKey
126
- * @param {string} [defaults.model] — default model
127
- * @param {string} [defaults.proxyUrl] — proxy URL (for CORS)
128
- * @param {string} [defaults.system] — default system prompt
129
- */
130
- export function createClient(defaults = {}) {
131
- return {
132
- chat: (opts) => chat({ ...defaults, ...opts }),
133
- stream: (opts) => streamChat({ ...defaults, ...opts }),
134
- };
135
- }
136
-
137
- // Re-export adapters for direct use
138
- export { anthropic, openai, gemini };
@@ -1,85 +0,0 @@
1
- /**
2
- * OpenAI Chat Completions API adapter.
3
- * Endpoint: https://api.openai.com/v1/chat/completions
4
- * Also compatible with: Groq, Together, Mistral, any OpenAI-compatible API.
5
- */
6
-
7
- import { readSSE } from './sse.js';
8
-
9
- const API_URL = 'https://api.openai.com/v1/chat/completions';
10
- const DEFAULT_MAX_TOKENS = 4096;
11
-
12
- export const openai = {
13
- name: 'openai',
14
-
15
- buildRequest(opts) {
16
- const messages = [];
17
- if (opts.system) messages.push({ role: 'system', content: opts.system });
18
- for (const msg of opts.messages) {
19
- messages.push({ role: msg.role, content: msg.content });
20
- }
21
-
22
- const body = {
23
- model: opts.model,
24
- messages,
25
- stream: !!opts.stream,
26
- };
27
- if (opts.maxTokens) body.max_tokens = opts.maxTokens;
28
- if (opts.temperature != null) body.temperature = opts.temperature;
29
- if (opts.stream) body.stream_options = { include_usage: true };
30
-
31
- return {
32
- url: opts.proxyUrl || API_URL,
33
- headers: {
34
- 'content-type': 'application/json',
35
- 'authorization': `Bearer ${opts.apiKey}`,
36
- },
37
- body,
38
- };
39
- },
40
-
41
- parseResponse(data) {
42
- const choice = data.choices?.[0];
43
- const text = choice?.message?.content ?? '';
44
- return {
45
- text,
46
- usage: { input: data.usage?.prompt_tokens ?? 0, output: data.usage?.completion_tokens ?? 0 },
47
- stopReason: choice?.finish_reason === 'stop' ? 'end' : (choice?.finish_reason ?? 'end'),
48
- };
49
- },
50
-
51
- async *parseStream(response) {
52
- let snapshot = '';
53
- let usage = { input: 0, output: 0 };
54
- let stopReason = 'end';
55
-
56
- for await (const event of readSSE(response.body)) {
57
- if (event.done) break;
58
- let data;
59
- try { data = JSON.parse(event.data); } catch { continue; }
60
-
61
- if (data.usage) {
62
- usage.input = data.usage.prompt_tokens ?? 0;
63
- usage.output = data.usage.completion_tokens ?? 0;
64
- }
65
-
66
- const choice = data.choices?.[0];
67
- if (!choice) continue;
68
-
69
- if (choice.finish_reason) {
70
- stopReason = choice.finish_reason === 'stop' ? 'end' : choice.finish_reason;
71
- }
72
-
73
- const delta = choice.delta;
74
- if (delta?.content) {
75
- snapshot += delta.content;
76
- yield { type: 'text', text: delta.content, snapshot };
77
- }
78
- if (delta?.reasoning_content) {
79
- yield { type: 'thinking', text: delta.reasoning_content };
80
- }
81
- }
82
-
83
- yield { type: 'done', text: snapshot, usage, stopReason };
84
- },
85
- };
@@ -1,50 +0,0 @@
1
- /**
2
- * SSE Parser — shared by Anthropic, OpenAI, and Gemini adapters.
3
- * Handles partial line buffering, double-newline splitting, and [DONE] detection.
4
- */
5
-
6
- export async function* readSSE(body) {
7
- const reader = body.getReader();
8
- const decoder = new TextDecoder();
9
- let buffer = '';
10
- try {
11
- while (true) {
12
- const { done, value } = await reader.read();
13
- if (done) break;
14
- buffer += decoder.decode(value, { stream: true });
15
- const { events, remainder } = parse(buffer);
16
- buffer = remainder;
17
- for (const event of events) yield event;
18
- }
19
- if (buffer.trim()) {
20
- const { events } = parse(buffer + '\n\n');
21
- for (const event of events) yield event;
22
- }
23
- } finally {
24
- reader.releaseLock();
25
- }
26
- }
27
-
28
- function parse(text) {
29
- const events = [];
30
- const parts = text.split(/\n\n|\r\n\r\n/);
31
- const remainder = parts.pop() ?? '';
32
- for (const part of parts) {
33
- const trimmed = part.trim();
34
- if (!trimmed) continue;
35
- let eventType;
36
- const dataLines = [];
37
- for (const line of trimmed.split(/\r?\n/)) {
38
- if (line.startsWith(':')) continue;
39
- if (line.startsWith('event:')) eventType = line.slice(6).trim();
40
- else if (line.startsWith('data:')) {
41
- const v = line.slice(5);
42
- dataLines.push(v.startsWith(' ') ? v.slice(1) : v);
43
- }
44
- }
45
- if (!dataLines.length) continue;
46
- const data = dataLines.join('\n');
47
- events.push({ event: eventType, data, done: data === '[DONE]' });
48
- }
49
- return { events, remainder };
50
- }
package/llm/llm-bridge.js DELETED
@@ -1,214 +0,0 @@
1
- /**
2
- * LLM Bridge — Wraps AdiaUI's llm module into the AdiaUI createAdapter() API.
3
- *
4
- * This is the single integration point between the AdiaUI pipeline and the
5
- * LLM module. It handles:
6
- * - Env var reading (VITE_* in browser, process.env in Node)
7
- * - CORS proxy routing in browser (Vite dev server at /api/llm/*)
8
- * - API translation (AdiaUI's simple { messages, systemPrompt } → llm module's interface)
9
- *
10
- * Consumers call createAdapter() and get an object with .complete() and .stream()
11
- * matching the AdiaUI pipeline interface.
12
- */
13
-
14
- import { StubLLMAdapter } from './llm-stub.js';
15
-
16
- // Lazy-loaded — ../llm/index.js uses Vite aliases that don't resolve in Node
17
- let _createClient = null;
18
- async function getCreateClient() {
19
- if (!_createClient) {
20
- try {
21
- const mod = await import('./adapters/index.js');
22
- _createClient = mod.createClient;
23
- } catch {
24
- _createClient = null;
25
- }
26
- }
27
- return _createClient;
28
- }
29
-
30
- // ── Environment ──────────────────────────────────────────────────────────
31
-
32
- function getEnv(key) {
33
- try {
34
- const env = import.meta.env;
35
- if (env) {
36
- const val = env[`VITE_${key}`] || env[key];
37
- if (val) return val;
38
- }
39
- } catch {}
40
- if (typeof process !== 'undefined' && process.env) {
41
- return process.env[key] || '';
42
- }
43
- return '';
44
- }
45
-
46
- const IS_BROWSER = typeof window !== 'undefined';
47
-
48
- function resolveBaseUrl(provider) {
49
- if (!IS_BROWSER) return undefined; // Let the module use its defaults
50
- const proxyMap = {
51
- anthropic: '/api/llm/anthropic/v1/messages',
52
- openai: '/api/llm/openai/v1/chat/completions',
53
- google: '/api/llm/google',
54
- };
55
- return proxyMap[provider];
56
- }
57
-
58
- // ── Factory ──────────────────────────────────────────────────────────────
59
-
60
- /**
61
- * Create an LLM adapter for the AdiaUI pipeline.
62
- *
63
- * Auto-detects provider from env vars. Returns an object with .complete()
64
- * and .stream() that match the AdiaUI interface (simple messages + systemPrompt).
65
- *
66
- * @param {object} [opts]
67
- * @param {string} [opts.provider] — 'anthropic' | 'openai' | 'google' | 'stub'
68
- * @param {string} [opts.apiKey] — explicit API key (overrides env)
69
- * @param {string} [opts.model] — model override
70
- * @returns {StubLLMAdapter | AdiaUILLMBridge}
71
- */
72
- export async function createAdapter(opts = {}) {
73
- const provider = opts.provider || getEnv('LLM_PROVIDER') || detectProvider();
74
- const model = opts.model || getEnv('LLM_MODEL') || undefined;
75
-
76
- if (provider === 'stub') return new StubLLMAdapter();
77
-
78
- // Resolve API key for the detected provider
79
- const apiKey = opts.apiKey || getEnv(`${provider.toUpperCase()}_API_KEY`) || getEnv('ANTHROPIC_API_KEY') || getEnv('OPENAI_API_KEY') || getEnv('GOOGLE_API_KEY');
80
-
81
- // No key found → fall back to stub
82
- if (!apiKey) {
83
- console.warn('LLM Bridge: No API keys found. Using stub adapter.');
84
- return new StubLLMAdapter();
85
- }
86
-
87
- const createClient = await getCreateClient();
88
- if (!createClient) {
89
- console.warn('LLM Bridge: LLM module not available. Using stub adapter.');
90
- return new StubLLMAdapter();
91
- }
92
-
93
- const proxyUrl = resolveBaseUrl(provider);
94
- const client = createClient({
95
- provider,
96
- apiKey,
97
- model: model || DEFAULT_MODELS[provider] || 'claude-sonnet-4-20250514',
98
- ...(proxyUrl ? { proxyUrl } : {}),
99
- });
100
-
101
- return new AdiaUILLMBridge(client, model || DEFAULT_MODELS[provider] || 'claude-sonnet-4-20250514', provider);
102
- }
103
-
104
- function detectProvider() {
105
- if (getEnv('ANTHROPIC_API_KEY')) return 'anthropic';
106
- if (getEnv('OPENAI_API_KEY')) return 'openai';
107
- if (getEnv('GOOGLE_API_KEY')) return 'google';
108
- return 'stub';
109
- }
110
-
111
- // ── Bridge class ─────────────────────────────────────────────────────────
112
-
113
- /** Default models per provider */
114
- const DEFAULT_MODELS = {
115
- anthropic: 'claude-sonnet-4-20250514',
116
- openai: 'gpt-4o',
117
- google: 'gemini-2.0-flash',
118
- };
119
-
120
- /**
121
- * Wraps the AdiaUI llm client to match the AdiaUI pipeline's simpler interface.
122
- *
123
- * AdiaUI calls: adapter.complete({ messages, systemPrompt })
124
- * LLM module expects: client.chat({ model, messages, system, ... })
125
- */
126
- class AdiaUILLMBridge {
127
- #client;
128
- #model;
129
- #provider;
130
-
131
- constructor(client, model, provider) {
132
- this.#client = client;
133
- this.#model = model;
134
- this.#provider = provider;
135
- }
136
-
137
- /**
138
- * Non-streaming completion. Matches AdiaUI interface.
139
- *
140
- * 32k max_tokens: A2UI JSON for moderately complex UIs (kanban, dashboard,
141
- * pricing table) routinely exceeds 8k. Truncation produced silent fallbacks
142
- * that the validator rubber-stamped at ~89/100 — see diagnosis report
143
- * 2026-04-19. Modern Claude/GPT/Gemini all support ≥32k output cleanly.
144
- *
145
- * @param {{ messages: { role: string, content: string }[], systemPrompt?: string }} opts
146
- * @returns {Promise<{ content: string, stopReason: string, usage: { inputTokens: number, outputTokens: number } }>}
147
- */
148
- async complete({ messages, systemPrompt }) {
149
- const response = await this.#client.chat({
150
- model: this.#model,
151
- messages,
152
- system: systemPrompt,
153
- maxTokens: 32768,
154
- // Anthropic-only: mark the system prompt as a cache breakpoint. No-op
155
- // on other providers (unknown opt silently ignored) and no-op below the
156
- // model's minimum cacheable size.
157
- cache: this.#provider === 'anthropic',
158
- });
159
- return {
160
- content: response.text,
161
- // 'max_tokens' / 'length' / 'MAX_TOKENS' (Gemini) signal truncation;
162
- // downstream parser uses this to refuse silent fallback rendering.
163
- stopReason: response.stopReason ?? 'end',
164
- usage: {
165
- inputTokens: response.usage?.input ?? 0,
166
- outputTokens: response.usage?.output ?? 0,
167
- cacheCreationTokens: response.usage?.cacheCreation ?? 0,
168
- cacheReadTokens: response.usage?.cacheRead ?? 0,
169
- },
170
- };
171
- }
172
-
173
- /**
174
- * Streaming completion. Matches AdiaUI interface.
175
- *
176
- * @param {{ messages: { role: string, content: string }[], systemPrompt?: string }} opts
177
- * @yields {{ type: 'text', content: string } | { type: 'done', stopReason: string, usage: { inputTokens: number, outputTokens: number, cacheCreationTokens: number, cacheReadTokens: number } }}
178
- */
179
- async *stream({ messages, systemPrompt }) {
180
- for await (const chunk of this.#client.stream({
181
- model: this.#model,
182
- messages,
183
- system: systemPrompt,
184
- maxTokens: 32768,
185
- cache: this.#provider === 'anthropic',
186
- })) {
187
- if (chunk.type === 'text') {
188
- yield { type: 'text', content: chunk.text };
189
- } else if (chunk.type === 'done') {
190
- // Surface the terminal stopReason + cache telemetry so the consumer
191
- // can detect max_tokens truncation and the dialog recorder can log
192
- // cache hit-rate per turn.
193
- yield {
194
- type: 'done',
195
- stopReason: chunk.stopReason ?? 'end',
196
- usage: {
197
- inputTokens: chunk.usage?.input ?? 0,
198
- outputTokens: chunk.usage?.output ?? 0,
199
- cacheCreationTokens: chunk.usage?.cacheCreation ?? 0,
200
- cacheReadTokens: chunk.usage?.cacheRead ?? 0,
201
- },
202
- };
203
- }
204
- // Other chunk types (thinking, error) are still available on the
205
- // underlying adapter but the AdiaUI pipeline doesn't consume them yet.
206
- }
207
- }
208
-
209
- /** Expose the underlying client for advanced use. */
210
- get adapter() { return this.#client; }
211
-
212
- /** Expose provider name for detection. */
213
- get provider() { return this.#provider; }
214
- }
package/llm/llm-stub.js DELETED
@@ -1,69 +0,0 @@
1
- /**
2
- * StubLLMAdapter — Deterministic LLM adapter for testing.
3
- *
4
- * Returns canned A2UI responses for known prompts. Implements the same
5
- * interface that a real LLM adapter would (complete, stream) so pipeline
6
- * code can develop against it without API keys.
7
- */
8
-
9
- export class StubLLMAdapter {
10
- /**
11
- * Complete a prompt and return a full A2UI response.
12
- *
13
- * @param {object} opts
14
- * @param {object[]} opts.messages — Chat messages (system + user turns)
15
- * @param {string} [opts.systemPrompt] — System prompt override
16
- * @returns {Promise<{ content: string, usage: { inputTokens: number, outputTokens: number } }>}
17
- */
18
- async complete({ messages, systemPrompt }) {
19
- const lastMessage = messages?.[messages.length - 1]?.content || '';
20
- const components = this.#buildResponse(lastMessage);
21
-
22
- return {
23
- content: JSON.stringify([
24
- {
25
- type: 'updateComponents',
26
- surfaceId: 'default',
27
- components,
28
- },
29
- ]),
30
- usage: {
31
- inputTokens: estimateTokens(JSON.stringify(messages)),
32
- outputTokens: estimateTokens(JSON.stringify(components)),
33
- },
34
- };
35
- }
36
-
37
- /**
38
- * Stream a response as an async iterable of chunks.
39
- *
40
- * @param {object} request — Same shape as complete()
41
- * @yields {{ type: 'text', content: string }}
42
- */
43
- async *stream(request) {
44
- const result = await this.complete(request);
45
- // Simulate progressive streaming by yielding the full response
46
- yield { type: 'text', content: result.content };
47
- }
48
-
49
- /**
50
- * Build a canned component tree from the intent text.
51
- * @param {string} intent
52
- * @returns {object[]}
53
- */
54
- #buildResponse(intent) {
55
- return [
56
- { id: 'root', component: 'Card', children: ['hdr', 'sec'] },
57
- { id: 'hdr', component: 'Header', children: ['title'] },
58
- { id: 'title', component: 'Text', variant: 'h3', textContent: 'Generated UI' },
59
- { id: 'sec', component: 'Section', children: ['col'] },
60
- { id: 'col', component: 'Column', children: ['desc'] },
61
- { id: 'desc', component: 'Text', variant: 'body', textContent: intent || 'No intent provided' },
62
- ];
63
- }
64
- }
65
-
66
- /** Rough token estimate (~4 chars per token) */
67
- function estimateTokens(text) {
68
- return Math.ceil((text?.length || 0) / 4);
69
- }