@adia-ai/adia-ui-forge 0.8.61 → 0.8.63

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 (142) hide show
  1. package/.claude-plugin/plugin.json +2 -2
  2. package/.codex-plugin/plugin.json +3 -3
  3. package/CHANGELOG.md +309 -274
  4. package/README.md +54 -18
  5. package/__init__.py +8 -8
  6. package/agents/a2ui-maintenance-agent.corpus.json +7 -7
  7. package/agents/a2ui-maintenance-agent.md +6 -6
  8. package/agents/a2ui-planner-agent.md +11 -11
  9. package/agents/demo-audit-agent.corpus.json +4 -4
  10. package/agents/demo-audit-agent.md +8 -9
  11. package/agents/framework-checker.md +10 -11
  12. package/agents/framework-planner-agent.md +9 -10
  13. package/agents/gen-ui-review-agent.corpus.json +4 -4
  14. package/agents/gen-ui-review-agent.md +8 -8
  15. package/agents/package-release-agent.corpus.json +4 -4
  16. package/agents/package-release-agent.md +9 -10
  17. package/agents/primitive-authoring-agent.corpus.json +1 -1
  18. package/agents/primitive-authoring-agent.md +5 -5
  19. package/agents/routing-corpus.json +46 -46
  20. package/codex.interface.json +1 -1
  21. package/commands/gen-ui-review.md +1 -1
  22. package/commands/package-release.md +2 -2
  23. package/package.json +1 -1
  24. package/plugin.yaml +2 -2
  25. package/prompts/gen-ui-review.md +1 -1
  26. package/prompts/package-release.md +2 -2
  27. package/references/contracts/a2ui-mcp-surface.md +7 -7
  28. package/references/contracts/migration-guide-format.md +6 -7
  29. package/references/shared/content-trust.md +3 -5
  30. package/skills/a2ui-maintenance/SKILL.md +13 -16
  31. package/skills/a2ui-maintenance/evals/evals.json +2 -2
  32. package/skills/a2ui-maintenance/evals/routing-corpus.json +1 -1
  33. package/skills/a2ui-maintenance/references/anti-patterns.md +10 -10
  34. package/skills/a2ui-maintenance/references/chunk-authoring.md +11 -12
  35. package/skills/a2ui-maintenance/references/corpus-discipline.md +13 -14
  36. package/skills/a2ui-maintenance/references/data-model-reactivity.md +22 -25
  37. package/skills/a2ui-maintenance/references/eval-diagnostics.md +21 -21
  38. package/skills/a2ui-maintenance/references/format-extension-decisions.md +12 -13
  39. package/skills/a2ui-maintenance/references/leverage-rules.md +6 -6
  40. package/skills/a2ui-maintenance/references/mcp-pipeline-ops.md +9 -10
  41. package/skills/a2ui-maintenance/references/mcp-tool-reference.md +11 -12
  42. package/skills/a2ui-maintenance/references/pipeline-overview.md +33 -38
  43. package/skills/a2ui-maintenance/references/semantic-fail-lifting.md +13 -14
  44. package/skills/a2ui-maintenance/references/strategy-engines.md +14 -16
  45. package/skills/a2ui-maintenance/references/surface-lifecycle.md +10 -11
  46. package/skills/a2ui-maintenance/references/zettel-calibration.md +14 -16
  47. package/skills/component-md-authoring/SKILL.md +20 -21
  48. package/skills/component-md-authoring/agents/openai.yaml +1 -1
  49. package/skills/cross-harness-authoring-standards/SKILL.md +19 -21
  50. package/skills/cross-harness-authoring-standards/agents/openai.yaml +1 -1
  51. package/skills/cross-harness-compatibility-standards/SKILL.md +34 -35
  52. package/skills/cross-harness-compatibility-standards/agents/openai.yaml +1 -1
  53. package/skills/demo-audit/SKILL.md +38 -38
  54. package/skills/demo-audit/evals/routing-corpus.json +1 -1
  55. package/skills/demo-audit/references/admin-shell-anatomy.md +21 -25
  56. package/skills/demo-audit/references/app-shell-pitfalls.md +11 -13
  57. package/skills/demo-audit/references/auto-fix-allowlist.md +3 -3
  58. package/skills/demo-audit/references/card-anatomy-sweep.md +10 -11
  59. package/skills/demo-audit/references/chat-shell-anatomy.md +25 -26
  60. package/skills/demo-audit/references/editor-shell-anatomy.md +25 -28
  61. package/skills/demo-audit/references/html-attr-sweep.md +12 -13
  62. package/skills/demo-audit/references/mode7-status-battery.md +4 -4
  63. package/skills/demo-audit/references/native-leak-annotations.md +8 -8
  64. package/skills/demo-audit/references/output-contract-worked-example.md +8 -8
  65. package/skills/demo-audit/references/probe-discipline-and-escalation.md +2 -2
  66. package/skills/demo-audit/references/visual-probe-triage.md +21 -23
  67. package/skills/gen-ui-review/SKILL.md +27 -27
  68. package/skills/gen-ui-review/agents/openai.yaml +1 -1
  69. package/skills/gen-ui-review/evals/routing-corpus.json +1 -1
  70. package/skills/gen-ui-review/references/corpus-html-patterns.md +28 -29
  71. package/skills/gen-ui-review/references/exit-gate-mechanics.md +5 -5
  72. package/skills/gen-ui-review/references/lookup-maintenance.md +2 -2
  73. package/skills/gen-ui-review/references/loop-protocol.md +25 -25
  74. package/skills/gen-ui-review/references/rubric-cosmetic.md +10 -10
  75. package/skills/gen-ui-review/references/rubric-decompose.md +15 -16
  76. package/skills/gen-ui-review/references/rubric-score.md +23 -24
  77. package/skills/gen-ui-review/references/scorecard-worked-examples.md +12 -12
  78. package/skills/gen-ui-review/references/scores.schema.json +2 -2
  79. package/skills/llm-client-maintenance/SKILL.md +12 -12
  80. package/skills/llm-client-maintenance/evals/routing-corpus.json +1 -1
  81. package/skills/llm-client-maintenance/references/adapter-contract.md +27 -27
  82. package/skills/llm-client-maintenance/references/add-a-provider.md +25 -25
  83. package/skills/llm-client-maintenance/references/bridge-facade.md +31 -31
  84. package/skills/llm-client-maintenance/references/browser-proxy-boundary.md +17 -17
  85. package/skills/llm-client-maintenance/references/model-registry.md +14 -14
  86. package/skills/llm-client-maintenance/references/streaming-sse.md +21 -21
  87. package/skills/package-release/SKILL.md +21 -23
  88. package/skills/package-release/evals/evals.json +2 -2
  89. package/skills/package-release/evals/routing-corpus.json +2 -2
  90. package/skills/package-release/references/authorization-model.md +6 -8
  91. package/skills/package-release/references/changelog-discipline.md +25 -27
  92. package/skills/package-release/references/cut-procedure.md +129 -136
  93. package/skills/package-release/references/gates-catalog.md +42 -43
  94. package/skills/package-release/references/independent-package-release.md +14 -15
  95. package/skills/package-release/references/invariants-detail.md +9 -9
  96. package/skills/package-release/references/mechanization.md +11 -12
  97. package/skills/package-release/references/migration-guide-authoring.md +20 -20
  98. package/skills/package-release/references/notes-authoring.md +20 -20
  99. package/skills/package-release/references/recovery-paths.md +28 -28
  100. package/skills/primitive-authoring/SKILL.md +19 -19
  101. package/skills/primitive-authoring/agents/openai.yaml +1 -1
  102. package/skills/primitive-authoring/evals/evals.json +2 -2
  103. package/skills/primitive-authoring/evals/routing-corpus.json +2 -2
  104. package/skills/primitive-authoring/references/INDEX.md +22 -22
  105. package/skills/primitive-authoring/references/anti-patterns.md +76 -77
  106. package/skills/primitive-authoring/references/api-contract.md +63 -65
  107. package/skills/primitive-authoring/references/authoring-cycle.md +47 -50
  108. package/skills/primitive-authoring/references/canonical-pattern-index.md +118 -118
  109. package/skills/primitive-authoring/references/code-style.md +50 -52
  110. package/skills/primitive-authoring/references/common-gotchas.md +23 -23
  111. package/skills/primitive-authoring/references/composite-demo-protocol.md +42 -45
  112. package/skills/primitive-authoring/references/css-patterns.md +97 -97
  113. package/skills/primitive-authoring/references/for-attribute-event-contract.md +12 -12
  114. package/skills/primitive-authoring/references/form-control-sizing.md +16 -18
  115. package/skills/primitive-authoring/references/lifecycle-patterns.md +42 -42
  116. package/skills/primitive-authoring/references/llm-bridge.md +21 -21
  117. package/skills/primitive-authoring/references/module-promotion.md +51 -51
  118. package/skills/primitive-authoring/references/primitive-audit.md +16 -16
  119. package/skills/primitive-authoring/references/shell-patterns.md +81 -81
  120. package/skills/primitive-authoring/references/svg-authoring.md +62 -66
  121. package/skills/primitive-authoring/references/token-contract.md +45 -49
  122. package/skills/primitive-authoring/references/trait-pages.md +8 -8
  123. package/skills/primitive-authoring/references/worked-example.md +39 -39
  124. package/skills/primitive-authoring/references/yaml-contract.md +139 -143
  125. package/skills/site-deployment/SKILL.md +25 -26
  126. package/skills/site-deployment/agents/openai.yaml +1 -1
  127. package/skills/site-deployment/evals/routing-corpus.json +1 -1
  128. package/skills/site-deployment/references/deploy-playbooks.md +51 -53
  129. package/skills/site-docs-authoring/SKILL.md +14 -14
  130. package/skills/site-docs-authoring/agents/openai.yaml +1 -1
  131. package/skills/site-docs-authoring/evals/evals.json +1 -1
  132. package/skills/site-docs-authoring/evals/routing-corpus.json +1 -1
  133. package/skills/site-docs-authoring/intent.md +18 -18
  134. package/skills/ssr-compatibility/SKILL.md +24 -25
  135. package/skills/ssr-compatibility/agents/openai.yaml +1 -1
  136. package/skills/ssr-compatibility/evals/evals.json +1 -1
  137. package/skills/ssr-compatibility/evals/routing-corpus.json +1 -1
  138. package/skills/ssr-compatibility/references/consumer-workarounds.md +22 -23
  139. package/skills/ssr-compatibility/references/failure-shapes.md +84 -91
  140. package/skills/ssr-compatibility/references/guard-patterns.md +54 -58
  141. package/skills/ssr-compatibility/references/status-ledger.md +17 -17
  142. package/skills/ssr-compatibility/references/test-without-linkedom.md +26 -27
@@ -1,27 +1,27 @@
1
1
  # Adapter contract
2
2
 
3
- The single shape every provider adapter implements. Source: `packages/llm/core/src/adapters/anthropic.ts` (the canonical adapter `openai.ts` and `gemini.ts` import their shared types from it), the facade `packages/llm/core/src/adapters/index.ts`, and the shared parser `packages/llm/core/src/adapters/sse.ts`.
3
+ The single shape every provider adapter implements. Source: `packages/llm/core/src/adapters/anthropic.ts` (the canonical adapter, `openai.ts` and `gemini.ts` import their shared types from it), the facade `packages/llm/core/src/adapters/index.ts`, and the shared parser `packages/llm/core/src/adapters/sse.ts`.
4
4
 
5
- This reference is about the adapter object its three methods, the request it builds, and the response/usage/`stopReason` it parses. The SSE-byte mechanics live in `streaming-sse.md`; the facade that dispatches to an adapter lives in `bridge-facade.md`.
5
+ This reference is about the adapter object, its three methods, the request it builds, and the response/usage/`stopReason` it parses. The SSE-byte mechanics live in `streaming-sse.md`; the facade that dispatches to an adapter lives in `bridge-facade.md`.
6
6
 
7
- Field tables below are a snapshot verified against `packages/llm/core/src/` on 2026-07-10 on any conflict, the source wins; re-grep before relying on an exact field name.
7
+ Field tables below are a snapshot verified against `packages/llm/core/src/` on 2026-07-10, on any conflict, the source wins; re-grep before relying on an exact field name.
8
8
 
9
9
  ---
10
10
 
11
- ## The adapter object three methods
11
+ ## The adapter object, three methods
12
12
 
13
13
  Each provider exports a single `const` (lowercase provider name) with a `name` literal plus three methods. The facade calls them; consumers never touch an adapter directly.
14
14
 
15
15
  | Member | Signature (see `anthropic.ts`) | Job |
16
16
  | --- | --- | --- |
17
17
  | `name` | `'anthropic' as const` (or `'openai'` / `'gemini'`) | Registry key + error messages |
18
- | `buildRequest(opts)` | `(BuildRequestOpts) => AdapterRequest` | Build the upstream `{ url, headers, body }` the single source of truth for upstream shape |
18
+ | `buildRequest(opts)` | `(BuildRequestOpts) => AdapterRequest` | Build the upstream `{ url, headers, body }`, the single source of truth for upstream shape |
19
19
  | `parseResponse(json)` | `(ProviderResponseBody) => AdapterResponse` | Map a non-streaming JSON body to `{ text, usage, stopReason }` |
20
20
  | `parseStream(response)` | `(Response) => AsyncGenerator<StreamChunk>` | Map an SSE `Response` to the `StreamChunk` union |
21
21
 
22
- The shared types (`AdapterRequest`, `AdapterUsage`, `AdapterResponse`, `StreamChunk`, `BuildRequestOpts`) are declared in `anthropic.ts` and re-exported from `index.ts`. `openai.ts` and `gemini.ts` `import type` them from `./anthropic.js`. **When you change a shared type, change it in `anthropic.ts`** the other two adapters and the facade inherit it.
22
+ The shared types (`AdapterRequest`, `AdapterUsage`, `AdapterResponse`, `StreamChunk`, `BuildRequestOpts`) are declared in `anthropic.ts` and re-exported from `index.ts`. `openai.ts` and `gemini.ts` `import type` them from `./anthropic.js`. **When you change a shared type, change it in `anthropic.ts`**, the other two adapters and the facade inherit it.
23
23
 
24
- ## `AdapterResponse` the normalized result
24
+ ## `AdapterResponse`, the normalized result
25
25
 
26
26
  `parseResponse` (and the terminal `done` chunk of `parseStream`) always returns:
27
27
 
@@ -37,25 +37,25 @@ The facade wraps this as `ChatResult` (same fields) for `chat()`. Each adapter i
37
37
  | `text` | first `content[]` block of `type === 'text'` | `choices[0].message.content` | join of `candidates[0].content.parts[].text` |
38
38
  | `usage.input` | `usage.input_tokens` | `usage.prompt_tokens` | `usageMetadata.promptTokenCount` |
39
39
  | `usage.output` | `usage.output_tokens` | `usage.completion_tokens` | `usageMetadata.candidatesTokenCount` |
40
- | `usage.cacheCreation` | `usage.cache_creation_input_tokens` | (absent) | (absent) |
41
- | `usage.cacheRead` | `usage.cache_read_input_tokens` | (absent) | (absent) |
42
- | `stopReason` | `stop_reason` (raw) | `finish_reason` (`stop` → `end`, else raw) | `finishReason` (raw `STOP`/`MAX_TOKENS`/`SAFETY`; `'end'` only when absent; fixed 2026-07-16, was hardcoded `'end'` hiding truncation) |
40
+ | `usage.cacheCreation` | `usage.cache_creation_input_tokens` |, (absent) |, (absent) |
41
+ | `usage.cacheRead` | `usage.cache_read_input_tokens` |, (absent) |, (absent) |
42
+ | `stopReason` | `stop_reason` (raw) | `finish_reason` (`stop` → `end`, else raw) | `finishReason` (raw, `STOP`/`MAX_TOKENS`/`SAFETY`; `'end'` only when absent; fixed 2026-07-16, was hardcoded `'end'` hiding truncation) |
43
43
 
44
44
  Every numeric field defaults to `0` via `?? 0`; missing cache fields stay absent for non-Anthropic providers. **A new adapter must fill `input` + `output`; cache fields are optional.**
45
45
 
46
- ## `stopReason` propagate raw, never normalize
46
+ ## `stopReason`, propagate raw, never normalize
47
47
 
48
48
  The `stopReason` string is part of the public contract because the downstream truncation detector reads it. Known values across providers:
49
49
 
50
- - `end` / `stop` / `STOP` clean completion.
51
- - `max_tokens` (Anthropic) / `length` (OpenAI) / `MAX_TOKENS` (Gemini) **truncation**; the consumer refuses silent fallback rendering.
52
- - `tool_use` (Anthropic) / `tool_calls` (OpenAI) tool-call paused output.
50
+ - `end` / `stop` / `STOP`, clean completion.
51
+ - `max_tokens` (Anthropic) / `length` (OpenAI) / `MAX_TOKENS` (Gemini), **truncation**; the consumer refuses silent fallback rendering.
52
+ - `tool_use` (Anthropic) / `tool_calls` (OpenAI), tool-call paused output.
53
53
 
54
- OpenAI's adapter maps only its own `finish_reason === 'stop'` to `end` and passes everything else through raw (see `openai.ts` `parseResponse`). Anthropic passes `stop_reason` through untouched. **Do not collapse the truncation values to `end`** that hides truncation from the consumer and is a defect, not a cleanup.
54
+ OpenAI's adapter maps only its own `finish_reason === 'stop'` to `end` and passes everything else through raw (see `openai.ts` `parseResponse`). Anthropic passes `stop_reason` through untouched. **Do not collapse the truncation values to `end`**: that hides truncation from the consumer and is a defect, not a cleanup.
55
55
 
56
- ## `buildRequest()` one source of truth for upstream shape
56
+ ## `buildRequest()`, one source of truth for upstream shape
57
57
 
58
- `buildRequest(opts: BuildRequestOpts)` returns `{ url, headers, body }`. It is called from two places direct mode and passthrough-proxy mode and **must produce the same upstream body + auth headers in both**. Don't fork it for proxy mode; the dispatcher swaps only the URL (see `browser-proxy-boundary.md`).
58
+ `buildRequest(opts: BuildRequestOpts)` returns `{ url, headers, body }`. It is called from two places, direct mode and passthrough-proxy mode, and **must produce the same upstream body + auth headers in both**. Don't fork it for proxy mode; the dispatcher swaps only the URL (see `browser-proxy-boundary.md`).
59
59
 
60
60
  Per-provider request facts grounded in the source:
61
61
 
@@ -68,9 +68,9 @@ Per-provider request facts grounded in the source:
68
68
  | Stream flag | `stream` in body | `stream` + `stream_options.include_usage` | encoded in the URL action |
69
69
  | Roles | `messages[]` verbatim | `messages[]` verbatim | `assistant` → `model`, else `user` |
70
70
 
71
- Gemini's URL is action-dependent: `generateContent` for non-streaming, `streamGenerateContent?alt=sse` for streaming (see `gemini.ts` the `action` ternary). The other two use one URL and a `stream` body flag.
71
+ Gemini's URL is action-dependent: `generateContent` for non-streaming, `streamGenerateContent?alt=sse` for streaming (see `gemini.ts`, the `action` ternary). The other two use one URL and a `stream` body flag.
72
72
 
73
- ## Anthropic prompt caching the `cache` opt
73
+ ## Anthropic prompt caching, the `cache` opt
74
74
 
75
75
  `BuildRequestOpts.cache` is Anthropic-specific. When `cache` is truthy, `anthropic.ts` wraps the system prompt as a cache-control block:
76
76
 
@@ -78,24 +78,24 @@ Gemini's URL is action-dependent: `generateContent` for non-streaming, `streamGe
78
78
  body.system = [{ type: 'text', text: opts.system, cache_control: { type: 'ephemeral' } }]
79
79
  ```
80
80
 
81
- `openai.ts` and `gemini.ts` ignore `cache` (they read only the fields they understand). The bridge sets `cache: this.#provider === 'anthropic'` so the adia-ui system prompt becomes a cache breakpoint. **A new adapter should ignore `cache` unless its provider has its own caching mechanism** then add a parallel branch in that adapter's `buildRequest` and surface the cache fields in `parseResponse`.
81
+ `openai.ts` and `gemini.ts` ignore `cache` (they read only the fields they understand). The bridge sets `cache: this.#provider === 'anthropic'` so the adia-ui system prompt becomes a cache breakpoint. **A new adapter should ignore `cache` unless its provider has its own caching mechanism**, then add a parallel branch in that adapter's `buildRequest` and surface the cache fields in `parseResponse`.
82
82
 
83
- ## `thinking` Anthropic-only request, two adapters surface a chunk
83
+ ## `thinking`, Anthropic-only request, two adapters surface a chunk
84
84
 
85
85
  `BuildRequestOpts.thinking` / `thinkingBudget` are read only by `anthropic.ts` (`body.thinking = { type: 'enabled', budget_tokens: opts.thinkingBudget ?? 10000 }`). On the streaming side, both Anthropic (`thinking_delta`) and OpenAI (`delta.reasoning_content`) emit a `{ type: 'thinking', text }` chunk; Gemini does not. Consumers may ignore `thinking` chunks, but they must not crash on them.
86
86
 
87
- ## Worked example auditing an adapter's `usage` mapping
87
+ ## Worked example, auditing an adapter's `usage` mapping
88
88
 
89
89
  Symptom: cache hit-rate telemetry reads `0` for Anthropic even though the API returned cache reads.
90
90
 
91
91
  1. Plan: verify target is a real `chat()` against Anthropic with a system prompt large enough to cache (≥1024 tok Sonnet/Opus, ≥2048 Haiku) and `cache: true`, run twice in the cache window.
92
- 2. Execute: inspect `anthropic.ts` `parseResponse` confirm `cacheRead` reads `usage.cache_read_input_tokens` (not a camelCase guess). For streaming, confirm `message_start` sets `usage.cacheCreation` / `usage.cacheRead` and `message_delta` sets `usage.output` (see `parseStream` in `anthropic.ts`).
92
+ 2. Execute: inspect `anthropic.ts` `parseResponse`, confirm `cacheRead` reads `usage.cache_read_input_tokens` (not a camelCase guess). For streaming, confirm `message_start` sets `usage.cacheCreation` / `usage.cacheRead` and `message_delta` sets `usage.output` (see `parseStream` in `anthropic.ts`).
93
93
  3. Verify: the second `chat()` returns `usage.cacheRead > 0`; the bridge's `cacheReadTokens` is non-zero. If it's still `0`, the bug is the field name, not the bridge.
94
94
 
95
95
  ## Cross-references
96
96
 
97
- - [streaming-sse.md](streaming-sse.md) the `StreamChunk` union, the shared SSE parser, and the per-provider event mapping
98
- - [bridge-facade.md](bridge-facade.md) how `chat()` / `streamChat()` resolve and call an adapter; the `ChatResult` wrapper
99
- - [browser-proxy-boundary.md](browser-proxy-boundary.md) why `buildRequest()` must stay proxy-agnostic
100
- - [add-a-provider.md](add-a-provider.md) the end-to-end recipe that uses this contract
97
+ - [streaming-sse.md](streaming-sse.md), the `StreamChunk` union, the shared SSE parser, and the per-provider event mapping
98
+ - [bridge-facade.md](bridge-facade.md), how `chat()` / `streamChat()` resolve and call an adapter; the `ChatResult` wrapper
99
+ - [browser-proxy-boundary.md](browser-proxy-boundary.md), why `buildRequest()` must stay proxy-agnostic
100
+ - [add-a-provider.md](add-a-provider.md), the end-to-end recipe that uses this contract
101
101
  - Source: `packages/llm/core/src/adapters/anthropic.ts` (canonical types + adapter), `openai.ts`, `gemini.ts`
@@ -1,6 +1,6 @@
1
1
  # Add a provider
2
2
 
3
- The end-to-end recipe for a 4th provider adapter (DeepSeek, Mistral, Cohere, an OpenAI-compatible gateway, …). Grounded in the existing three adapters under `packages/llm/core/src/adapters/` and the facade `packages/llm/core/src/adapters/index.ts`. Read `adapter-contract.md` first this recipe wires the contract it defines.
3
+ The end-to-end recipe for a 4th provider adapter (DeepSeek, Mistral, Cohere, an OpenAI-compatible gateway, …). Grounded in the existing three adapters under `packages/llm/core/src/adapters/` and the facade `packages/llm/core/src/adapters/index.ts`. Read `adapter-contract.md` first, this recipe wires the contract it defines.
4
4
 
5
5
  Adding a provider is **additive** to the public surface (a new adapter, a new `MODELS` group, a new `detectProvider` branch). It does not break existing consumers if you follow the contract.
6
6
 
@@ -8,20 +8,20 @@ Adding a provider is **additive** to the public surface (a new adapter, a new `M
8
8
 
9
9
  ## Decide first: is this OpenAI-compatible?
10
10
 
11
- Many providers (Groq, Together, Mistral, any OpenAI-compatible gateway) speak the OpenAI Chat Completions wire format. `openai.ts` already notes this. If the new provider is OpenAI-compatible, you may not need a new adapter at all you may only need a `detectProvider` branch and a base-URL override. A genuinely new wire format (different request body, different SSE event shape) needs a new adapter. Confirm against the provider's API docs before writing code.
11
+ Many providers (Groq, Together, Mistral, any OpenAI-compatible gateway) speak the OpenAI Chat Completions wire format. `openai.ts` already notes this. If the new provider is OpenAI-compatible, you may not need a new adapter at all, you may only need a `detectProvider` branch and a base-URL override. A genuinely new wire format (different request body, different SSE event shape) needs a new adapter. Confirm against the provider's API docs before writing code.
12
12
 
13
- ## Step 1 write the adapter
13
+ ## Step 1, write the adapter
14
14
 
15
15
  Create `packages/llm/core/src/adapters/<name>.ts` exporting one `const` matching the contract (model on `anthropic.ts`):
16
16
 
17
17
  - `name: '<name>' as const`.
18
- - `buildRequest(opts: BuildRequestOpts): AdapterRequest` return `{ url, headers, body }`. Set the auth header the provider expects. Map `opts.system`, `opts.messages`, `opts.maxTokens` (default to the package's `32768` unless the provider's ceiling is lower), `opts.temperature`, and the `stream` flag into the provider's body shape.
19
- - `parseResponse(json): AdapterResponse` map to `{ text, usage: { input, output }, stopReason }`. Pass `stopReason` through RAW (don't normalize).
20
- - `parseStream(response): AsyncGenerator<StreamChunk>` guard `response.body`, consume `readSSE` from `./sse.js`, accumulate `snapshot` + `usage`, yield `text` deltas with the running `snapshot`, and yield exactly one terminal `done`.
18
+ - `buildRequest(opts: BuildRequestOpts): AdapterRequest`, return `{ url, headers, body }`. Set the auth header the provider expects. Map `opts.system`, `opts.messages`, `opts.maxTokens` (default to the package's `32768` unless the provider's ceiling is lower), `opts.temperature`, and the `stream` flag into the provider's body shape.
19
+ - `parseResponse(json): AdapterResponse`, map to `{ text, usage: { input, output }, stopReason }`. Pass `stopReason` through RAW (don't normalize).
20
+ - `parseStream(response): AsyncGenerator<StreamChunk>`, guard `response.body`, consume `readSSE` from `./sse.js`, accumulate `snapshot` + `usage`, yield `text` deltas with the running `snapshot`, and yield exactly one terminal `done`.
21
21
 
22
- `import type { AdapterRequest, AdapterResponse, AdapterUsage, StreamChunk, BuildRequestOpts } from './anthropic.js';` reuse the canonical types; don't redeclare them.
22
+ `import type { AdapterRequest, AdapterResponse, AdapterUsage, StreamChunk, BuildRequestOpts } from './anthropic.js';`, reuse the canonical types; don't redeclare them.
23
23
 
24
- ## Step 2 register in the facade
24
+ ## Step 2, register in the facade
25
25
 
26
26
  Edit `packages/llm/core/src/adapters/index.ts`:
27
27
 
@@ -36,15 +36,15 @@ Then add a `detectProvider` branch so model ids route without an explicit `provi
36
36
  if (m.includes('<id-substring>') || m.startsWith('<name>/')) return 'myprovider';
37
37
  ```
38
38
 
39
- Support both conventions a substring (`m.includes(...)`) and a `provider/model` prefix (`m.startsWith('<name>/')`) to match how the existing branches behave (see `model-registry.md`).
39
+ Support both conventions, a substring (`m.includes(...)`) and a `provider/model` prefix (`m.startsWith('<name>/')`), to match how the existing branches behave (see `model-registry.md`).
40
40
 
41
- ## Step 3 subpath export (usually automatic)
41
+ ## Step 3, subpath export (usually automatic)
42
42
 
43
43
  `package.json` already has `"./adapters/*"` as a glob export, so `@adia-ai/llm/adapters/<name>` resolves without an edit. Only add a named subpath (like `./bridge` / `./models`) if you want a stable short import.
44
44
 
45
- ## Step 4 add to the model registry
45
+ ## Step 4, add to the model registry
46
46
 
47
- Edit `packages/llm/core/src/models.ts` add a `ModelGroup`:
47
+ Edit `packages/llm/core/src/models.ts`, add a `ModelGroup`:
48
48
 
49
49
  ```text
50
50
  { label: 'MyProvider', options: [ { value: '<model-id>', label: '<display name>' } ] }
@@ -52,28 +52,28 @@ Edit `packages/llm/core/src/models.ts` — add a `ModelGroup`:
52
52
 
53
53
  Keep the `[{ label, options: [{ value, label }] }]` shape exactly (the `<chat-input-ui>` setter depends on it). The `value` must be a model id your Step 2 `detectProvider` branch classifies. Don't change `DEFAULT_MODEL` to the new provider unless you intend a default switch (keep it cheap/fast).
54
54
 
55
- ## Step 5 wire the bridge default + browser route (if browser-routable)
55
+ ## Step 5, wire the bridge default + browser route (if browser-routable)
56
56
 
57
57
  Edit `packages/llm/core/src/llm-bridge.ts`:
58
58
 
59
- - Add `myprovider: '<default-model-id>'` to `DEFAULT_MODELS` (the per-provider fallback `createAdapter()` uses when no model is supplied distinct from `models.ts`'s `DEFAULT_MODEL`; see `model-registry.md`).
60
- - **INVARIANT: every keyed map in the bridge (`DEFAULT_MODELS`, `resolveBaseUrl`'s proxyMap, `detectProvider`'s return) uses the SAME key as the adapters registry** (`providers` in `adapters/index.ts`). A mismatched key compiles fine and throws at runtime (`Unknown provider`) the gh#268 L-1 bug class. If a marketing name differs from the registry key (Google/Gemini), normalize the alias at the top of `createAdapter()` and use the registry key everywhere below.
59
+ - Add `myprovider: '<default-model-id>'` to `DEFAULT_MODELS` (the per-provider fallback `createAdapter()` uses when no model is supplied, distinct from `models.ts`'s `DEFAULT_MODEL`; see `model-registry.md`).
60
+ - **INVARIANT: every keyed map in the bridge (`DEFAULT_MODELS`, `resolveBaseUrl`'s proxyMap, `detectProvider`'s return) uses the SAME key as the adapters registry** (`providers` in `adapters/index.ts`). A mismatched key compiles fine and throws at runtime (`Unknown provider`), the gh#268 L-1 bug class. If a marketing name differs from the registry key (Google/Gemini), normalize the alias at the top of `createAdapter()` and use the registry key everywhere below.
61
61
  - If the provider should work in the browser, add a `resolveBaseUrl` entry whose path is `/api/llm/<name>/...` so it matches the passthrough regex (see `browser-proxy-boundary.md`). The deployed app's same-origin proxy must have a matching route that injects the server-side key.
62
62
 
63
- ## Step 6 caching + thinking (only if the provider supports them)
63
+ ## Step 6, caching + thinking (only if the provider supports them)
64
64
 
65
- `BuildRequestOpts.cache` and `thinking` are currently Anthropic-shaped. Leave `cache` ignored unless the provider has its own prompt-cache mechanism then add a parallel branch in your `buildRequest` and surface the cache fields in `parseResponse`/`parseStream`. Same for `thinking`: only emit a `{ type: 'thinking', text }` chunk if the provider streams reasoning deltas (OpenAI does, via `delta.reasoning_content`; Gemini does not).
65
+ `BuildRequestOpts.cache` and `thinking` are currently Anthropic-shaped. Leave `cache` ignored unless the provider has its own prompt-cache mechanism, then add a parallel branch in your `buildRequest` and surface the cache fields in `parseResponse`/`parseStream`. Same for `thinking`: only emit a `{ type: 'thinking', text }` chunk if the provider streams reasoning deltas (OpenAI does, via `delta.reasoning_content`; Gemini does not).
66
66
 
67
- ## Step 7 verify against the real provider
67
+ ## Step 7, verify against the real provider
68
68
 
69
69
  This is the verify target. Do not declare done on a clean compile alone:
70
70
 
71
- 1. `npm run build -w @adia-ai/llm` (repo root; `tsc --build`) the new adapter compiles with no type error against the shared types.
71
+ 1. `npm run build -w @adia-ai/llm` (repo root; `tsc --build`), the new adapter compiles with no type error against the shared types.
72
72
  2. A real `chat({ provider: 'myprovider', apiKey: '<key>', model: '<model-id>', messages: [...] })` returns non-empty `text` and a sane `usage` (`input`/`output` > 0) and a raw `stopReason`.
73
73
  3. A real `streamChat(...)` shows ordered `text` deltas with a growing `snapshot`, a terminal `done` carrying final `usage`, and an `error` chunk on a forced failure (bad key).
74
74
  4. `detectProvider('<model-id>')` returns `'myprovider'` (auto-detection works with no explicit provider).
75
75
 
76
- ## Worked example adding an OpenAI-compatible gateway
76
+ ## Worked example, adding an OpenAI-compatible gateway
77
77
 
78
78
  A gateway that speaks the OpenAI wire format with a different base URL and a `gw-` model prefix.
79
79
 
@@ -83,8 +83,8 @@ A gateway that speaks the OpenAI wire format with a different base URL and a `gw
83
83
 
84
84
  ## Cross-references
85
85
 
86
- - [adapter-contract.md](adapter-contract.md) the three-method object + the `usage` / `stopReason` mapping this recipe implements
87
- - [streaming-sse.md](streaming-sse.md) the `StreamChunk` protocol the new `parseStream` must satisfy
88
- - [model-registry.md](model-registry.md) the `MODELS` group + `detectProvider` branch
89
- - [browser-proxy-boundary.md](browser-proxy-boundary.md) the `resolveBaseUrl` entry + passthrough regex
90
- - [bridge-facade.md](bridge-facade.md) `DEFAULT_MODELS` and the lazy-load gate
86
+ - [adapter-contract.md](adapter-contract.md), the three-method object + the `usage` / `stopReason` mapping this recipe implements
87
+ - [streaming-sse.md](streaming-sse.md), the `StreamChunk` protocol the new `parseStream` must satisfy
88
+ - [model-registry.md](model-registry.md), the `MODELS` group + `detectProvider` branch
89
+ - [browser-proxy-boundary.md](browser-proxy-boundary.md), the `resolveBaseUrl` entry + passthrough regex
90
+ - [bridge-facade.md](bridge-facade.md), `DEFAULT_MODELS` and the lazy-load gate
@@ -6,7 +6,7 @@ This reference covers the facade/bridge contract and the stub. The proxy-flavor
6
6
 
7
7
  ---
8
8
 
9
- ## The public barrel what `@adia-ai/llm` exports
9
+ ## The public barrel, what `@adia-ai/llm` exports
10
10
 
11
11
  `index.ts` re-exports exactly four things; this is the package's public API surface:
12
12
 
@@ -17,21 +17,21 @@ export { StubLLMAdapter } from './llm-stub.js';
17
17
  export { createAdapter } from './llm-bridge.js';
18
18
  ```
19
19
 
20
- Subpath exports (`package.json` `exports`): `.` (the barrel), `./adapters/*` (each adapter, glob), `./bridge`, `./models`, `./stub`. The `./adapters/*` glob means **a new adapter file is exported automatically** no `package.json` edit needed unless you want a named subpath.
20
+ Subpath exports (`package.json` `exports`): `.` (the barrel), `./adapters/*` (each adapter, glob), `./bridge`, `./models`, `./stub`. The `./adapters/*` glob means **a new adapter file is exported automatically**, no `package.json` edit needed unless you want a named subpath.
21
21
 
22
- ## Build order the package's `index.js` is a tsc artifact
22
+ ## Build order: the package's `index.js` is a tsc artifact
23
23
 
24
24
  `npm run build -w @adia-ai/llm` (repo root) compiles the package; the emitted `index.js` is gitignored. Two downstream consequences:
25
25
 
26
- - **A fresh git worktree can't build the repo's JS bundles until `@adia-ai/llm` is built** the tsc artifact doesn't exist in a fresh checkout.
26
+ - **A fresh git worktree can't build the repo's JS bundles until `@adia-ai/llm` is built**, the tsc artifact doesn't exist in a fresh checkout.
27
27
  - **After any source change here that component bundles consume, rebuild in order:** `npm run build -w @adia-ai/llm` FIRST (`build:bundle-js` resolves the package's `index.js`), then `npm run build:bundle-css` / `npm run build:bundle-js`. The `check:css-bundles-fresh` / `check:js-bundles-fresh` gates are blocking in CI, and `build:site` does NOT rebuild these package bundles.
28
28
 
29
- ## Layer 1 the standalone facade (`chat` / `streamChat`)
29
+ ## Layer 1, the standalone facade (`chat` / `streamChat`)
30
30
 
31
31
  `chat(opts: ChatOpts): Promise<ChatResult>` and `streamChat(opts): AsyncGenerator<StreamChunk>` are the provider-agnostic functions. Both follow the same arc in `index.ts`:
32
32
 
33
- 1. `resolveAdapter(opts)` uses `opts.provider` or `detectProvider(opts.model)`; throws if neither resolves (see `model-registry.md`).
34
- 2. Build the request: if `opts.proxyUrl` is set, branch on `isPassthroughProxy(proxyUrl)` (passthrough vs smart proxy see `browser-proxy-boundary.md`); otherwise call `adapter.buildRequest({ ...opts, stream })` directly.
33
+ 1. `resolveAdapter(opts)`, uses `opts.provider` or `detectProvider(opts.model)`; throws if neither resolves (see `model-registry.md`).
34
+ 2. Build the request: if `opts.proxyUrl` is set, branch on `isPassthroughProxy(proxyUrl)` (passthrough vs smart proxy, see `browser-proxy-boundary.md`); otherwise call `adapter.buildRequest({ ...opts, stream })` directly.
35
35
  3. `fetch(url, { method: 'POST', headers, body: JSON.stringify(body), signal })`.
36
36
  4. On `!res.ok`: `chat` throws `Error(err.error.message || '<adapter> API error <status>')`; `streamChat` yields an `error` chunk instead of throwing.
37
37
  5. `chat` returns `adapter.parseResponse(json)`; `streamChat` delegates to `adapter.parseStream(res)`.
@@ -44,58 +44,58 @@ ChatOpts = { model; messages; apiKey; provider?; system?; maxTokens?; temperat
44
44
  ChatResult = { text: string; usage: AdapterUsage; stopReason: string }
45
45
  ```
46
46
 
47
- `ChatResult` is structurally identical to `AdapterResponse` the facade returns the adapter's parsed response unchanged. **Changing `ChatOpts` / `ChatResult` is a breaking change for every consumer**; adding an optional `ChatOpts` field is additive.
47
+ `ChatResult` is structurally identical to `AdapterResponse`, the facade returns the adapter's parsed response unchanged. **Changing `ChatOpts` / `ChatResult` is a breaking change for every consumer**; adding an optional `ChatOpts` field is additive.
48
48
 
49
- ## Layer 1b `createClient` (reusable instance with defaults)
49
+ ## Layer 1b, `createClient` (reusable instance with defaults)
50
50
 
51
51
  `createClient(defaults: Partial<ChatOpts> = {}): LLMClient` returns `{ chat, stream }` that merge `defaults` under each call's opts (`{ ...defaults, ...opts }`). The merge is shallow and call-opts-win. Use it to bake in `provider` + `apiKey` + `proxyUrl` once. `LLMClient` is the interface the bridge wraps.
52
52
 
53
- ## Layer 2 the bridge (`createAdapter` → `AdiaUILLMBridge`)
53
+ ## Layer 2, the bridge (`createAdapter` → `AdiaUILLMBridge`)
54
54
 
55
55
  `llm-bridge.ts` adapts the facade to the adia-ui pipeline's simpler interface. The pipeline calls `adapter.complete({ messages, systemPrompt })` and `adapter.stream({ messages, systemPrompt })`; the bridge translates to the facade's `chat({ model, messages, system, … })`.
56
56
 
57
57
  `createAdapter(opts): Promise<StubLLMAdapter | AdiaUILLMBridge>` (async):
58
58
 
59
- - Resolves `provider` in precedence order: `opts.provider` → the model id via `detectProviderFromModel(model)` (exported from `adapters/index.ts`) → `LLM_PROVIDER` env → env-key `detectProvider()`. The model id outranking env detection is load-bearing a model-picker selection ("gemini-2.5-flash") must decide the provider, or every non-default model routes to the env-detected provider's API and 404s (live bug fixed 2026-07-16). Resolves the model from `opts.model` / `LLM_MODEL` env / per-provider `DEFAULT_MODELS`.
60
- - Reads the API key from `opts.apiKey` or the provider's own `KEY_ENVS` entry **per-provider isolated**, never a cross-provider fallback chain (`anthropic: [ANTHROPIC_API_KEY]`, `openai: [OPENAI_API_KEY]`, `gemini: [GEMINI_API_KEY, GOOGLE_API_KEY]` Gemini's only exception is its own legacy alias, not another provider's key). PR #274 removed an earlier cross-provider fallback as a key-leak risk; don't reintroduce it.
61
- - **No key on a production browser host → routes through the same-origin smart proxy for the SELECTED provider** (`createBrowserProxyBridge`), not the stub the proxy holds the real key server-side. **No key anywhere else (Node, local dev, non-production host) → returns `StubLLMAdapter`** (with a console warning). Key present → builds a real bridge.
62
- - Lazy-loads the adapters via `getCreateClient()` see "Lazy-load gate" below.
59
+ - Resolves `provider` in precedence order: `opts.provider` → the model id via `detectProviderFromModel(model)` (exported from `adapters/index.ts`) → `LLM_PROVIDER` env → env-key `detectProvider()`. The model id outranking env detection is load-bearing, a model-picker selection ("gemini-2.5-flash") must decide the provider, or every non-default model routes to the env-detected provider's API and 404s (live bug fixed 2026-07-16). Resolves the model from `opts.model` / `LLM_MODEL` env / per-provider `DEFAULT_MODELS`.
60
+ - Reads the API key from `opts.apiKey` or the provider's own `KEY_ENVS` entry, **per-provider isolated**, never a cross-provider fallback chain (`anthropic: [ANTHROPIC_API_KEY]`, `openai: [OPENAI_API_KEY]`, `gemini: [GEMINI_API_KEY, GOOGLE_API_KEY]`, Gemini's only exception is its own legacy alias, not another provider's key). PR #274 removed an earlier cross-provider fallback as a key-leak risk; don't reintroduce it.
61
+ - **No key on a production browser host → routes through the same-origin smart proxy for the SELECTED provider** (`createBrowserProxyBridge`), not the stub, the proxy holds the real key server-side. **No key anywhere else (Node, local dev, non-production host) → returns `StubLLMAdapter`** (with a console warning). Key present → builds a real bridge.
62
+ - Lazy-loads the adapters via `getCreateClient()`, see "Lazy-load gate" below.
63
63
 
64
64
  The `AdiaUILLMBridge` class re-shapes both directions:
65
65
 
66
- - **`complete()`** calls `chat()` with `maxTokens: 32768` and `cache: provider === 'anthropic'`, then returns `{ content, stopReason, usage: { inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens } }` note the field rename from the facade's `usage.input` to the pipeline's `usage.inputTokens`.
67
- - **`stream()`** iterates `client.stream()`, re-emits `text` chunks as `{ type: 'text', content }`, and re-emits `done` as `{ type: 'done', stopReason, usage: {…Tokens} }`. Chunk types it doesn't consume (`thinking`, `error`) fall through silently when you add a chunk type, decide whether the bridge should surface it.
66
+ - **`complete()`** calls `chat()` with `maxTokens: 32768` and `cache: provider === 'anthropic'`, then returns `{ content, stopReason, usage: { inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens } }`, note the field rename from the facade's `usage.input` to the pipeline's `usage.inputTokens`.
67
+ - **`stream()`** iterates `client.stream()`, re-emits `text` chunks as `{ type: 'text', content }`, and re-emits `done` as `{ type: 'done', stopReason, usage: {…Tokens} }`. Chunk types it doesn't consume (`thinking`, `error`) fall through silently, when you add a chunk type, decide whether the bridge should surface it.
68
68
 
69
- ### The `maxTokens: 32768` default is intentional don't lower it
69
+ ### The `maxTokens: 32768` default is intentional, don't lower it
70
70
 
71
- Both `complete()` and `stream()` hardcode `maxTokens: 32768`. A2UI JSON for moderately complex UIs (kanban, dashboard, pricing table) routinely exceeds 8k; an 8k cap produced silent truncation that the validator rubber-stamped at ~89/100. The adapters' own `DEFAULT_MAX_TOKENS` (Anthropic + Gemini) is also `32768`. If a new provider has a lower output ceiling, surface it as a model-level constraint in `MODELS` **do not lower the 32k bridge default**.
71
+ Both `complete()` and `stream()` hardcode `maxTokens: 32768`. A2UI JSON for moderately complex UIs (kanban, dashboard, pricing table) routinely exceeds 8k; an 8k cap produced silent truncation that the validator rubber-stamped at ~89/100. The adapters' own `DEFAULT_MAX_TOKENS` (Anthropic + Gemini) is also `32768`. If a new provider has a lower output ceiling, surface it as a model-level constraint in `MODELS`, **do not lower the 32k bridge default**.
72
72
 
73
- ### Lazy-load gate don't break Node/SSR consumers
73
+ ### Lazy-load gate, don't break Node/SSR consumers
74
74
 
75
- `createAdapter` reaches the adapters through `getCreateClient()`, which `await import('./adapters/index.js')` inside a `try`/`catch` (falling back to the stub on failure) and memoizes the result. This is load-bearing: `llm-bridge.ts` is browser/SSR-facing, and a top-level static import of the adapters can throw at module-load time in pure-Node contexts (Vite-alias / `node:` resolution). **If you add a module under `adapters/` that imports `node:` builtins, keep the lazy gate intact** top-level imports break SSR.
75
+ `createAdapter` reaches the adapters through `getCreateClient()`, which `await import('./adapters/index.js')` inside a `try`/`catch` (falling back to the stub on failure) and memoizes the result. This is load-bearing: `llm-bridge.ts` is browser/SSR-facing, and a top-level static import of the adapters can throw at module-load time in pure-Node contexts (Vite-alias / `node:` resolution). **If you add a module under `adapters/` that imports `node:` builtins, keep the lazy gate intact**, top-level imports break SSR.
76
76
 
77
- ## The stub `StubLLMAdapter`
77
+ ## The stub, `StubLLMAdapter`
78
78
 
79
- `llm-stub.ts` is the deterministic, no-API-key adapter pipeline code develops against. It mirrors the bridge's interface `complete({ messages, systemPrompt })` and `async *stream(request)` so consumers swap it in transparently.
79
+ `llm-stub.ts` is the deterministic, no-API-key adapter pipeline code develops against. It mirrors the bridge's interface, `complete({ messages, systemPrompt })` and `async *stream(request)`, so consumers swap it in transparently.
80
80
 
81
81
  Contract the stub must keep:
82
82
 
83
83
  - **`complete()` returns parseable A2UI.** It builds a canned component tree and returns `{ content: JSON.stringify([{ type: 'updateComponents', surfaceId: 'default', components }]), usage: { inputTokens, outputTokens } }`. The `content` must stay valid A2UI JSON or pipeline parsing breaks.
84
- - **`stream()` yields the same content as one `text` chunk.** It calls `complete()` and yields `{ type: 'text', content: result.content }` a single chunk that simulates streaming.
85
- - **`usage` is a rough estimate.** `estimateTokens` is `~chars/4`; it's deterministic, not exact, and that's fine the stub exists for offline development, not telemetry fidelity.
84
+ - **`stream()` yields the same content as one `text` chunk.** It calls `complete()` and yields `{ type: 'text', content: result.content }`, a single chunk that simulates streaming.
85
+ - **`usage` is a rough estimate.** `estimateTokens` is `~chars/4`; it's deterministic, not exact, and that's fine, the stub exists for offline development, not telemetry fidelity.
86
86
 
87
- The stub's `complete()` result shape (`{ content, usage: { inputTokens, outputTokens } }`) matches the bridge's `complete()` minus the cache fields keep them aligned when the bridge's shape changes so the stub stays a drop-in.
87
+ The stub's `complete()` result shape (`{ content, usage: { inputTokens, outputTokens } }`) matches the bridge's `complete()` minus the cache fields, keep them aligned when the bridge's shape changes so the stub stays a drop-in.
88
88
 
89
- ## Worked example `createAdapter()` returns the stub when a key IS set
89
+ ## Worked example, `createAdapter()` returns the stub when a key IS set
90
90
 
91
91
  1. Plan: verify target is a `createAdapter({ provider: 'anthropic', apiKey: '<real>' })` returning an `AdiaUILLMBridge` whose `.provider === 'anthropic'`, and `complete()` returning real `content`.
92
- 2. Execute: trace `createAdapter` `provider !== 'stub'` so it resolves the key; if the key is falsy it warns and returns the stub. Check the env-var path (`<PROVIDER>_API_KEY` then the Anthropic/OpenAI/Google fallbacks) and confirm `getCreateClient()` didn't fall back to `null` (lazy import failed → stub).
92
+ 2. Execute: trace `createAdapter`, `provider !== 'stub'` so it resolves the key; if the key is falsy it warns and returns the stub. Check the env-var path (`<PROVIDER>_API_KEY` then the Anthropic/OpenAI/Google fallbacks) and confirm `getCreateClient()` didn't fall back to `null` (lazy import failed → stub).
93
93
  3. Verify: with a real key, `createAdapter` returns a bridge (not a stub), `complete()` returns provider text, and `usage.cacheCreationTokens` / `cacheReadTokens` are populated for Anthropic.
94
94
 
95
95
  ## Cross-references
96
96
 
97
- - [adapter-contract.md](adapter-contract.md) what `resolveAdapter` / `buildRequest` / `parseResponse` do
98
- - [browser-proxy-boundary.md](browser-proxy-boundary.md) the `proxyUrl` dispatch and the production-host stub path
99
- - [model-registry.md](model-registry.md) `DEFAULT_MODEL` vs the bridge's `DEFAULT_MODELS`
100
- - [streaming-sse.md](streaming-sse.md) the `StreamChunk` union the bridge re-shapes
97
+ - [adapter-contract.md](adapter-contract.md), what `resolveAdapter` / `buildRequest` / `parseResponse` do
98
+ - [browser-proxy-boundary.md](browser-proxy-boundary.md), the `proxyUrl` dispatch and the production-host stub path
99
+ - [model-registry.md](model-registry.md), `DEFAULT_MODEL` vs the bridge's `DEFAULT_MODELS`
100
+ - [streaming-sse.md](streaming-sse.md), the `StreamChunk` union the bridge re-shapes
101
101
  - Source: `packages/llm/core/src/adapters/index.ts`, `packages/llm/core/src/llm-bridge.ts`, `packages/llm/core/src/llm-stub.ts`, `packages/llm/core/src/index.ts`
@@ -13,28 +13,28 @@ This is the single most error-prone area of the package: `proxyUrl` is overloade
13
13
  | Mode | When | Body shape | Auth |
14
14
  | --- | --- | --- | --- |
15
15
  | **Direct** | `proxyUrl` unset | `adapter.buildRequest()` upstream body | Adapter's own auth header (the real key) |
16
- | **Smart proxy** | `proxyUrl` set, NOT matching the passthrough regex | Provider-neutral `{ provider, model, messages, system?, maxTokens?, temperature?, thinking?, stream }` | `content-type` only the proxy holds the key server-side |
16
+ | **Smart proxy** | `proxyUrl` set, NOT matching the passthrough regex | Provider-neutral `{ provider, model, messages, system?, maxTokens?, temperature?, thinking?, stream }` | `content-type` only, the proxy holds the key server-side |
17
17
  | **Passthrough proxy** | `proxyUrl` matches `/api/llm/<provider>(/...)` | Real upstream body (`adapter.buildRequest()`), URL swapped to the proxy | Adapter's own auth header, forwarded verbatim by the dumb proxy |
18
18
 
19
19
  The dispatcher (in `index.ts`): `opts.proxyUrl ? (isPassthroughProxy(opts.proxyUrl) ? passthroughRequest(...) : proxyRequest(...)) : adapter.buildRequest(...)`.
20
20
 
21
- ## `isPassthroughProxy` the URL-shape classifier
21
+ ## `isPassthroughProxy`, the URL-shape classifier
22
22
 
23
23
  ```text
24
24
  PASSTHROUGH_PROXY_RE = /\/api\/llm\/[a-z]+(\/|$)/
25
25
  isPassthroughProxy(url) = typeof url === 'string' && PASSTHROUGH_PROXY_RE.test(url)
26
26
  ```
27
27
 
28
- Anything matching `/api/llm/<provider>/` is a **passthrough** proxy (a dumb URL rewriter e.g. the Vite dev server forwarding to `https://api.<provider>.com/...`). Everything else is treated as a **smart** proxy (a server that speaks the provider-neutral protocol and holds the key). **This regex is the entire contract** between the two flavors if you change the dev-server proxy path, update the regex, and vice versa.
28
+ Anything matching `/api/llm/<provider>/` is a **passthrough** proxy (a dumb URL rewriter, e.g. the Vite dev server forwarding to `https://api.<provider>.com/...`). Everything else is treated as a **smart** proxy (a server that speaks the provider-neutral protocol and holds the key). **This regex is the entire contract** between the two flavors, if you change the dev-server proxy path, update the regex, and vice versa.
29
29
 
30
- ## `proxyRequest` vs `passthroughRequest` the two builders
30
+ ## `proxyRequest` vs `passthroughRequest`, the two builders
31
31
 
32
- - **`proxyRequest(opts, stream)`** builds the provider-neutral body `{ provider, model, messages, stream }` plus the optional `system` / `maxTokens` / `temperature` / `thinking`, with headers `content-type: application/json` only. **No `apiKey` in the body** the smart proxy holds the key. URL is `opts.proxyUrl`.
33
- - **`passthroughRequest(opts, stream)`** calls `adapter.buildRequest({ ...opts, stream })` (the real upstream body + the adapter's auth headers) and then swaps ONLY the upstream **origin**: the proxy prefix (everything in `proxyUrl` up to the `/api/llm/<provider>` segment) is joined with `buildRequest()`'s own path + query. This preserves URL-encoded routing gemini carries `models/<model>:<action>?alt=sse` in the path; overwriting the whole URL with a static `proxyUrl` (the pre-2026-07-16 behavior) dropped it and every gemini passthrough call hit the proxy root.
32
+ - **`proxyRequest(opts, stream)`** builds the provider-neutral body `{ provider, model, messages, stream }` plus the optional `system` / `maxTokens` / `temperature` / `thinking`, with headers `content-type: application/json` only. **No `apiKey` in the body**: the smart proxy holds the key. URL is `opts.proxyUrl`.
33
+ - **`passthroughRequest(opts, stream)`** calls `adapter.buildRequest({ ...opts, stream })` (the real upstream body + the adapter's auth headers) and then swaps ONLY the upstream **origin**: the proxy prefix (everything in `proxyUrl` up to the `/api/llm/<provider>` segment) is joined with `buildRequest()`'s own path + query. This preserves URL-encoded routing, gemini carries `models/<model>:<action>?alt=sse` in the path; overwriting the whole URL with a static `proxyUrl` (the pre-2026-07-16 behavior) dropped it and every gemini passthrough call hit the proxy root.
34
34
 
35
35
  So `buildRequest()` is the single source of upstream shape for BOTH direct mode and passthrough mode (see `adapter-contract.md`). The only thing passthrough changes is the URL origin. **Don't fork `buildRequest` for proxy mode.**
36
36
 
37
- ## The browser bridge `resolveBaseUrl` + the proxy map
37
+ ## The browser bridge, `resolveBaseUrl` + the proxy map
38
38
 
39
39
  In the browser, `llm-bridge.ts` routes through a same-origin passthrough proxy. `resolveBaseUrl(provider)` returns `undefined` outside the browser (let the adapter use its default upstream URL) and, in the browser, maps provider → prefix (the upstream API path comes from `buildRequest()`, never from this map):
40
40
 
@@ -44,21 +44,21 @@ openai → /api/llm/openai
44
44
  gemini → /api/llm/gemini
45
45
  ```
46
46
 
47
- A `proxyUrl` that already includes the API path (the legacy `/api/llm/anthropic/v1/messages` shape) still works the prefix extraction stops at the provider segment and re-appends the identical upstream path.
47
+ A `proxyUrl` that already includes the API path (the legacy `/api/llm/anthropic/v1/messages` shape) still works, the prefix extraction stops at the provider segment and re-appends the identical upstream path.
48
48
 
49
49
  These paths match `PASSTHROUGH_PROXY_RE`, so the facade routes them through `passthroughRequest`. **A new browser-routable provider needs a `resolveBaseUrl` entry whose path matches the passthrough regex.**
50
50
 
51
- ## Production-host path real calls with no client key
51
+ ## Production-host path, real calls with no client key
52
52
 
53
53
  `isProductionHost()` returns `true` when the browser is on a non-local host (not `localhost` / `127.0.0.1` / `0.0.0.0` / `*.local` / private `10.` / `192.168.` / `172.16–31.` ranges). When `createAdapter()` detects `provider === 'stub'` (no env vars in the browser) BUT `IS_BROWSER && isProductionHost()`, it calls `createBrowserProxyBridge('anthropic', …)` instead of returning the stub:
54
54
 
55
55
  - It builds a real client with a **sentinel** `apiKey: 'browser-uses-server-side-proxy-key'` so the non-empty-key check passes.
56
- - The sentinel **never reaches the upstream provider** the same-origin proxy strips the incoming auth header and injects its own server-side key.
56
+ - The sentinel **never reaches the upstream provider**, the same-origin proxy strips the incoming auth header and injects its own server-side key.
57
57
  - This is how a deployed app makes real LLM calls without ever exposing a key to the browser.
58
58
 
59
59
  **Preserve this path.** Removing it makes production browsers fall back to the stub (canned UI) instead of real generation.
60
60
 
61
- ## Key-in-browser safety the one-shot warning
61
+ ## Key-in-browser safety, the one-shot warning
62
62
 
63
63
  In direct/passthrough browser mode with a real `apiKey`, the key is sent verbatim in `x-api-key` / `Authorization` headers and is readable in DevTools. `createAdapter()` logs a styled, deduplicated (window-flag `__adia_llm_key_warning_shown`) warning naming the provider and a masked key, stating this is local-dev only. This is intentional friction:
64
64
 
@@ -67,13 +67,13 @@ In direct/passthrough browser mode with a real `apiKey`, the key is sent verbati
67
67
 
68
68
  Keep the warning and the dedup flag when touching `createAdapter()`.
69
69
 
70
- ## Worked example "401 in the browser, but `node` works fine"
70
+ ## Worked example, "401 in the browser, but `node` works fine"
71
71
 
72
72
  1. Plan: verify target is a real browser `streamChat()` through the proxy returning `text` deltas, and the same call in Node (direct) succeeding.
73
- 2. Execute the usual cause is a proxy-flavor mismatch:
73
+ 2. Execute: the usual cause is a proxy-flavor mismatch:
74
74
  - Is `proxyUrl` matching `PASSTHROUGH_PROXY_RE`? If the dev server is a passthrough but the URL doesn't match the regex, the facade builds a provider-neutral body and sends it to a dumb proxy that expects the raw upstream shape → upstream 401/400.
75
75
  - For passthrough, confirm the adapter's auth header is present (`passthroughRequest` forwards `buildRequest` headers). For smart proxy, confirm NO key is in the body and the server injects it.
76
- - SSE buffering: a proxy that buffers responses breaks streaming confirm the proxy flushes SSE (this is a transport bug, surfaced as a "never emits `done`" in `streaming-sse.md`).
76
+ - SSE buffering: a proxy that buffers responses breaks streaming, confirm the proxy flushes SSE (this is a transport bug, surfaced as a "never emits `done`" in `streaming-sse.md`).
77
77
  3. Verify: browser `streamChat()` streams; Node direct works; no real key is visible in the browser Network panel on a production host.
78
78
 
79
79
  ## Pitfalls
@@ -85,7 +85,7 @@ Keep the warning and the dedup flag when touching `createAdapter()`.
85
85
 
86
86
  ## Cross-references
87
87
 
88
- - [adapter-contract.md](adapter-contract.md) `buildRequest()` as the single upstream-shape source
89
- - [bridge-facade.md](bridge-facade.md) `createAdapter()` and the stub fallback
90
- - [streaming-sse.md](streaming-sse.md) SSE-through-a-proxy buffering symptoms
88
+ - [adapter-contract.md](adapter-contract.md), `buildRequest()` as the single upstream-shape source
89
+ - [bridge-facade.md](bridge-facade.md), `createAdapter()` and the stub fallback
90
+ - [streaming-sse.md](streaming-sse.md), SSE-through-a-proxy buffering symptoms
91
91
  - Source: `isPassthroughProxy` / `proxyRequest` / `passthroughRequest` in `packages/llm/core/src/adapters/index.ts`; `resolveBaseUrl` / `isProductionHost` / `createBrowserProxyBridge` in `packages/llm/core/src/llm-bridge.ts`
@@ -2,15 +2,15 @@
2
2
 
3
3
  The shared model catalog and provider detection. Source: `packages/llm/core/src/models.ts` (the `MODELS` catalog + `DEFAULT_MODEL`) and the `detectProvider()` function in `packages/llm/core/src/adapters/index.ts`.
4
4
 
5
- Concrete ids/groups below are a snapshot verified against the source on 2026-07-10 on any conflict, the source wins.
5
+ Concrete ids/groups below are a snapshot verified against the source on 2026-07-10, on any conflict, the source wins.
6
6
 
7
7
  ---
8
8
 
9
9
  ## What the registry is for
10
10
 
11
- `models.ts` is the one source for the model list three surfaces previously duplicated. It is exported as a subpath (`@adia-ai/llm/models`) and as a top-level re-export from `index.ts`. Its consumer is the `<chat-input-ui>` web-module's `models` setter a grouped-options structure rendered by an internal `<select-ui>` with `<optgroup>`s.
11
+ `models.ts` is the one source for the model list three surfaces previously duplicated. It is exported as a subpath (`@adia-ai/llm/models`) and as a top-level re-export from `index.ts`. Its consumer is the `<chat-input-ui>` web-module's `models` setter, a grouped-options structure rendered by an internal `<select-ui>` with `<optgroup>`s.
12
12
 
13
- ## The `MODELS` shape do not deviate
13
+ ## The `MODELS` shape, do not deviate
14
14
 
15
15
  `models.ts` exports:
16
16
 
@@ -23,9 +23,9 @@ DEFAULT_MODEL: string
23
23
 
24
24
  `MODELS` is a 2-D grouped array: an outer list of provider groups, each with a `label` (the provider name shown as the `<optgroup>` label) and an `options[]` of `{ value, label }` pairs (the `value` is the model id sent to the API; the `label` is the human display name).
25
25
 
26
- **This exact shape is a contract.** The `<chat-input-ui>.models` setter expects `[{ label, options: [{ value, label }] }]`. Flattening it, renaming the keys, or nesting differently breaks the chat input. The current groups (as authored in `models.ts`) are `Anthropic`, `OpenAI`, `Google` adding a group is additive; restructuring is breaking.
26
+ **This exact shape is a contract.** The `<chat-input-ui>.models` setter expects `[{ label, options: [{ value, label }] }]`. Flattening it, renaming the keys, or nesting differently breaks the chat input. The current groups (as authored in `models.ts`) are `Anthropic`, `OpenAI`, `Google`, adding a group is additive; restructuring is breaking.
27
27
 
28
- ## `DEFAULT_MODEL` keep it cheap and present
28
+ ## `DEFAULT_MODEL`, keep it cheap and present
29
29
 
30
30
  `DEFAULT_MODEL` is the value `<chat-input-ui>` selects on load. Two rules:
31
31
 
@@ -43,25 +43,25 @@ When a `chat()` / `streamChat()` call omits an explicit `provider`, the facade i
43
43
  | `gemini` | includes `gemini` OR starts with `google/` |
44
44
  | `null` | none of the above → the facade throws "Cannot detect provider…; set provider explicitly" |
45
45
 
46
- **So a new model id in `MODELS` must either match an existing `detectProvider` branch or ship with a new branch** otherwise every consumer that relies on auto-detection breaks for that model. When you add a provider, add both the `MODELS` group AND the `detectProvider` branch (see `add-a-provider.md`).
46
+ **So a new model id in `MODELS` must either match an existing `detectProvider` branch or ship with a new branch**, otherwise every consumer that relies on auto-detection breaks for that model. When you add a provider, add both the `MODELS` group AND the `detectProvider` branch (see `add-a-provider.md`).
47
47
 
48
48
  Note the two id-naming conventions both flow through detection: a substring convention (`claude…`, `gpt…`, `gemini…`) and a `provider/model` prefix convention (`anthropic/…`, `openai/…`, `google/…`). A new provider should support both for forward-compatibility.
49
49
 
50
- ## The bridge's `DEFAULT_MODELS` is a separate map keep it in sync
50
+ ## The bridge's `DEFAULT_MODELS` is a separate map, keep it in sync
51
51
 
52
- `llm-bridge.ts` carries its own `DEFAULT_MODELS` (per-provider fallback used by `createAdapter()` when no model is supplied e.g. `anthropic → claude-sonnet-4-6`, `openai → gpt-4o`, `gemini → gemini-2.5-flash`). This is **distinct** from `models.ts`'s `DEFAULT_MODEL` (the chat-input default). They serve different surfaces. **The bridge's map MUST be keyed by the adapters-registry keys** (`anthropic`/`openai`/`gemini`) a 2026-07-16 live bug (gh#268 L-1) came from the bridge keying `google` while the registry keyed `gemini`: `createAdapter({provider:'google'})` always threw, and auto-detection returned an always-throwing bridge. `'google'` survives only as a normalized-away input alias in `createAdapter()`:
52
+ `llm-bridge.ts` carries its own `DEFAULT_MODELS` (per-provider fallback used by `createAdapter()` when no model is supplied, e.g. `anthropic → claude-sonnet-4-6`, `openai → gpt-4o`, `gemini → gemini-2.5-flash`). This is **distinct** from `models.ts`'s `DEFAULT_MODEL` (the chat-input default). They serve different surfaces. **The bridge's map MUST be keyed by the adapters-registry keys** (`anthropic`/`openai`/`gemini`), a 2026-07-16 live bug (gh#268 L-1) came from the bridge keying `google` while the registry keyed `gemini`: `createAdapter({provider:'google'})` always threw, and auto-detection returned an always-throwing bridge. `'google'` survives only as a normalized-away input alias in `createAdapter()`:
53
53
 
54
- - `models.ts:DEFAULT_MODEL` what the chat-input UI pre-selects (cheap/fast).
55
- - `llm-bridge.ts:DEFAULT_MODELS[provider]` what `createAdapter()` falls back to per provider when the caller passes none.
54
+ - `models.ts:DEFAULT_MODEL`, what the chat-input UI pre-selects (cheap/fast).
55
+ - `llm-bridge.ts:DEFAULT_MODELS[provider]`, what `createAdapter()` falls back to per provider when the caller passes none.
56
56
 
57
57
  When you add a provider, add an entry to the bridge's `DEFAULT_MODELS` too (see `bridge-facade.md`). When you retire a model id, check both maps.
58
58
 
59
- ## Worked example adding a model to an existing provider
59
+ ## Worked example, adding a model to an existing provider
60
60
 
61
61
  Add a new Anthropic model to the chat-input dropdown.
62
62
 
63
63
  1. Plan: verify target is `npm run build -w @adia-ai/llm` (the package compiles) plus a `chat()` with the new id resolving to the `anthropic` provider and returning text.
64
- 2. Execute: add `{ value: '<new-claude-id>', label: '<display name>' }` to the `Anthropic` group's `options[]` in `models.ts`. No `detectProvider` change needed the id includes `claude`, so the existing branch matches.
64
+ 2. Execute: add `{ value: '<new-claude-id>', label: '<display name>' }` to the `Anthropic` group's `options[]` in `models.ts`. No `detectProvider` change needed, the id includes `claude`, so the existing branch matches.
65
65
  3. Verify: the build passes; the `MODELS` shape is unchanged structurally; a `chat({ model: '<new-claude-id>', … })` with no explicit provider resolves Anthropic and returns non-empty `text`. If `DEFAULT_MODEL` should point at the new id, confirm it's a value that now exists.
66
66
 
67
67
  ## Pitfalls
@@ -72,6 +72,6 @@ Add a new Anthropic model to the chat-input dropdown.
72
72
 
73
73
  ## Cross-references
74
74
 
75
- - [bridge-facade.md](bridge-facade.md) `createAdapter()` and its per-provider `DEFAULT_MODELS`
76
- - [add-a-provider.md](add-a-provider.md) the recipe that adds a `MODELS` group + a `detectProvider` branch together
75
+ - [bridge-facade.md](bridge-facade.md), `createAdapter()` and its per-provider `DEFAULT_MODELS`
76
+ - [add-a-provider.md](add-a-provider.md), the recipe that adds a `MODELS` group + a `detectProvider` branch together
77
77
  - Source: `packages/llm/core/src/models.ts`, `detectProvider()` in `packages/llm/core/src/adapters/index.ts`, `DEFAULT_MODELS` in `packages/llm/core/src/llm-bridge.ts`