@adia-ai/adia-ui-forge 0.8.3 → 0.8.4

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adia-forge",
3
- "version": "0.8.3",
3
+ "version": "0.8.4",
4
4
  "description": "Maintain the adia-ui (@adia-ai) framework itself \u2014 author primitives and shells, run the A2UI generation pipeline and its corpus, review gen-UI quality, sweep QA, cut releases, deploy. The maintainer counterpart to adia-factory (the consumer/app-author plugin).",
5
5
  "author": {
6
6
  "name": "Kim",
package/CHANGELOG.md CHANGED
@@ -1,4 +1,10 @@
1
1
  # Changelog — adia-forge
2
+
3
+ ## [Unreleased]
4
+
5
+ ### Changed
6
+ - **`adia-llm-internals` references re-synced to the shipped fixes**: `bridge-facade.md` documents the new provider-resolution precedence (model id outranks env detection, `detectProviderFromModel`), `browser-proxy-boundary.md` documents origin-only passthrough URL swapping + the provider-prefix proxy map, `adapter-contract.md`/`model-registry.md`/`add-a-provider.md` carry the gemini raw-stopReason and keys-must-match-registry invariants.
7
+
2
8
  ## [0.8.3] — 2026-07-16
3
9
 
4
10
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adia-ai/adia-ui-forge",
3
- "version": "0.8.3",
3
+ "version": "0.8.4",
4
4
  "description": "Maintain the adia-ui (@adia-ai) framework itself \u2014 author primitives and shells, run the A2UI generation pipeline and its corpus, review gen-UI quality, sweep QA, cut releases, deploy. The maintainer counterpart to adia-factory (the consumer/app-author plugin).",
5
5
  "keywords": [
6
6
  "adia-ui",
@@ -39,7 +39,7 @@ The facade wraps this as `ChatResult` (same fields) for `chat()`. Each adapter i
39
39
  | `usage.output` | `usage.output_tokens` | `usage.completion_tokens` | `usageMetadata.candidatesTokenCount` |
40
40
  | `usage.cacheCreation` | `usage.cache_creation_input_tokens` | — (absent) | — (absent) |
41
41
  | `usage.cacheRead` | `usage.cache_read_input_tokens` | — (absent) | — (absent) |
42
- | `stopReason` | `stop_reason` (raw) | `finish_reason` (`stop` → `end`, else raw) | hardcoded `'end'` |
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
 
@@ -57,6 +57,7 @@ Keep the `[{ label, options: [{ value, label }] }]` shape exactly (the `<chat-in
57
57
  Edit `packages/llm/src/llm-bridge.ts`:
58
58
 
59
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.
60
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.
61
62
 
62
63
  ## Step 6 — caching + thinking (only if the provider supports them)
@@ -56,7 +56,7 @@ ChatResult = { text: string; usage: AdapterUsage; stopReason: string }
56
56
 
57
57
  `createAdapter(opts): Promise<StubLLMAdapter | AdiaUILLMBridge>` (async):
58
58
 
59
- - Resolves `provider` from `opts.provider` / `LLM_PROVIDER` env / `detectProvider()`; resolves the model from `opts.model` / `LLM_MODEL` env / per-provider `DEFAULT_MODELS`.
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
60
  - Reads the API key from `opts.apiKey` or the provider's `*_API_KEY` env var (with Anthropic/OpenAI/Google fallbacks).
61
61
  - **No key → returns `StubLLMAdapter`** (with a console warning). Key present → builds a real bridge.
62
62
  - Lazy-loads the adapters via `getCreateClient()` — see "Lazy-load gate" below.
@@ -30,20 +30,22 @@ Anything matching `/api/llm/<provider>/` is a **passthrough** proxy (a dumb URL
30
30
  ## `proxyRequest` vs `passthroughRequest` — the two builders
31
31
 
32
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 in `opts.proxyUrl` as the URL. The proxy forwards the bytes verbatim.
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
- 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. **Don't fork `buildRequest` for proxy mode.**
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
37
  ## The browser bridge — `resolveBaseUrl` + the proxy map
38
38
 
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:
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
 
41
41
  ```text
42
- anthropic → /api/llm/anthropic/v1/messages
43
- openai → /api/llm/openai/v1/chat/completions
44
- google → /api/llm/google
42
+ anthropic → /api/llm/anthropic
43
+ openai → /api/llm/openai
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.
48
+
47
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.**
48
50
 
49
51
  ## Production-host path — real calls with no client key
@@ -49,7 +49,7 @@ Note the two id-naming conventions both flow through detection: a substring conv
49
49
 
50
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-…`, `openai → gpt-4o`, `google → gemini-2.0-flash`). This is **distinct** from `models.ts`'s `DEFAULT_MODEL` (the chat-input default). They serve different surfaces:
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
54
  - `models.ts:DEFAULT_MODEL` — what the chat-input UI pre-selects (cheap/fast).
55
55
  - `llm-bridge.ts:DEFAULT_MODELS[provider]` — what `createAdapter()` falls back to per provider when the caller passes none.