@ggui-ai/ui-gen 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/dist/adapters/generation-dispatch.d.ts +9 -2
  2. package/dist/adapters/generation-dispatch.js +693 -89
  3. package/dist/adapters/generation-dispatch.js.map +1 -1
  4. package/dist/adapters/index.js +28 -11
  5. package/dist/adapters/index.js.map +1 -1
  6. package/dist/advanced/index.d.ts +5 -3
  7. package/dist/advanced/index.js +714 -101
  8. package/dist/advanced/index.js.map +1 -1
  9. package/dist/blueprint-validator.js +267 -3
  10. package/dist/blueprint-validator.js.map +1 -1
  11. package/dist/boilerplate.js +14 -2
  12. package/dist/boilerplate.js.map +1 -1
  13. package/dist/check/index.d.ts +1 -1
  14. package/dist/check/index.js +11 -1
  15. package/dist/check/index.js.map +1 -1
  16. package/dist/{create-ui-generator-B9kV4U9h.d.ts → create-ui-generator-CF0EL0Li.d.ts} +13 -1
  17. package/dist/{dispatch-Deo9qzrG.d.ts → dispatch-D75M-OfZ.d.ts} +1 -1
  18. package/dist/evaluation/axis-checks/index.d.ts +3 -3
  19. package/dist/evaluation/axis-checks/index.js +101 -0
  20. package/dist/evaluation/axis-checks/index.js.map +1 -1
  21. package/dist/evaluation/axis-checks/registry.d.ts +1 -1
  22. package/dist/evaluation/axis-checks/registry.js +101 -0
  23. package/dist/evaluation/axis-checks/registry.js.map +1 -1
  24. package/dist/evaluation/index.d.ts +46 -3
  25. package/dist/evaluation/index.js +358 -17
  26. package/dist/evaluation/index.js.map +1 -1
  27. package/dist/harness/check/runtime-render/index.d.ts +2 -2
  28. package/dist/harness/check/runtime-render/index.js +268 -3
  29. package/dist/harness/check/runtime-render/index.js.map +1 -1
  30. package/dist/harness/check/runtime-render/render-check-worker.d.ts +2 -0
  31. package/dist/harness/check/runtime-render/render-check-worker.js +1332 -0
  32. package/dist/harness/check/runtime-render/render-check-worker.js.map +1 -0
  33. package/dist/harness/index.d.ts +2 -2
  34. package/dist/harness/index.js +263 -18
  35. package/dist/harness/index.js.map +1 -1
  36. package/dist/harness/llm-trace-sink.d.ts +1 -1
  37. package/dist/harness/llm-trace-sink.js.map +1 -1
  38. package/dist/harness/prompts.js +17 -3
  39. package/dist/harness/prompts.js.map +1 -1
  40. package/dist/harness/result-types.d.ts +1 -1
  41. package/dist/harness/runtime.d.ts +9 -2
  42. package/dist/harness/runtime.js +20 -6
  43. package/dist/harness/runtime.js.map +1 -1
  44. package/dist/harness/types-public.d.ts +1 -1
  45. package/dist/harness/types-public.js +14 -2
  46. package/dist/harness/types-public.js.map +1 -1
  47. package/dist/harness/validator-trace-sink.d.ts +1 -1
  48. package/dist/index.d.ts +5 -3
  49. package/dist/index.js +708 -95
  50. package/dist/index.js.map +1 -1
  51. package/dist/{llm-router-1TyI0_TV.d.ts → llm-router-BEVtbAb9.d.ts} +61 -4
  52. package/dist/patch.d.ts +1 -1
  53. package/dist/provider-adapter.d.ts +16 -6
  54. package/dist/provider-adapter.js.map +1 -1
  55. package/dist/providers/index.d.ts +19 -6
  56. package/dist/providers/index.js.map +1 -1
  57. package/dist/{render-check-CNZjHD5g.d.ts → render-check-BZoIE8-u.d.ts} +21 -1
  58. package/dist/{types-public-C36k1q6s.d.ts → types-public-DIpjC-OA.d.ts} +1 -1
  59. package/dist/workflows.d.ts +1 -1
  60. package/package.json +12 -12
  61. package/src/adapters/generation-dispatch.ts +9 -0
  62. package/src/adapters/openrouter/client.ts +2 -2
  63. package/src/adapters/openrouter/types.ts +10 -0
  64. package/src/boilerplate/generate.ts +10 -2
  65. package/src/boilerplate/json-schema-ts.ts +22 -2
  66. package/src/boilerplate/system-prompt.ts +1 -0
  67. package/src/compile.ts +1 -1
  68. package/src/contract-context.ts +10 -2
  69. package/src/create-ui-generator.ts +14 -0
  70. package/src/design-system-docs.ts +6 -4
  71. package/src/evaluation/axis-checks/checks/state-ui-affordance.ts +162 -0
  72. package/src/evaluation/axis-checks/registry.ts +2 -0
  73. package/src/evaluation/index.ts +11 -0
  74. package/src/evaluation/llm-evaluator.ts +14 -1
  75. package/src/evaluation/visual-evaluator.ts +44 -65
  76. package/src/harness/check/runtime-render/adapter.ts +18 -0
  77. package/src/harness/check/runtime-render/index.ts +8 -0
  78. package/src/harness/check/runtime-render/prepare-mockup.ts +14 -0
  79. package/src/harness/check/runtime-render/production-act-shim.ts +158 -0
  80. package/src/harness/check/runtime-render/render-check-host.ts +201 -0
  81. package/src/harness/check/runtime-render/render-check-worker.ts +80 -0
  82. package/src/harness/check/runtime-render/render-check.ts +112 -1
  83. package/src/harness/coding/run-eval-round.ts +61 -5
  84. package/src/harness/index.ts +1 -1
  85. package/src/harness/llm-router.ts +397 -18
  86. package/src/harness/llm-trace-sink.ts +2 -1
  87. package/src/harness/runtime.ts +7 -0
  88. package/src/index.ts +5 -5
  89. package/src/provider-adapter.ts +16 -6
  90. package/src/providers/bedrock.ts +5 -4
  91. package/src/providers/index.ts +16 -4
  92. package/src/tools/render-check.ts +19 -7
@@ -25,6 +25,35 @@ interface AgentConfig {
25
25
  /** Anthropic-only: force the Bedrock-IAM client instead of the direct API client. */
26
26
  readonly useBedrock?: boolean;
27
27
  };
28
+ /**
29
+ * Observer invoked once per retried attempt when `apiCall()` (#489)
30
+ * retries a rate-limited (HTTP 429) provider call. Fires AFTER the
31
+ * retry decision is made and BEFORE the delay is awaited — never on
32
+ * the terminal failure/success of the whole call. Purely an
33
+ * observability hook: it never influences whether or how long
34
+ * `apiCall()` retries. If the observer itself throws, `apiCall()`
35
+ * catches it, logs a `console.warn`, and proceeds to the retry
36
+ * delay unaffected — a broken observer cannot turn a recoverable
37
+ * 429 into an unrelated hard failure. Absent (default) is a no-op;
38
+ * `apiCall()` still logs every retry via `console.warn` regardless.
39
+ */
40
+ onRetry?: (info: ProviderRetryInfo) => void;
41
+ }
42
+ /** One retried attempt's context — passed to `AgentConfig.onRetry`. */
43
+ interface ProviderRetryInfo {
44
+ readonly provider: AgentConfig['provider'];
45
+ /** 1-based index of this retry (1 = first retry, 2 = second, …). */
46
+ readonly attempt: number;
47
+ /** Total retries `apiCall()` will attempt before giving up. */
48
+ readonly maxAttempts: number;
49
+ /** HTTP status of the failed call, when the caught error carries one. */
50
+ readonly status: number | undefined;
51
+ /** Provider-supplied `Retry-After` value in seconds, when present and within the cap. */
52
+ readonly retryAfterSec: number | undefined;
53
+ /** Delay actually awaited before the next attempt. */
54
+ readonly delayMs: number;
55
+ /** One-line error summary (same shape as the existing `console.error` line). */
56
+ readonly message: string;
28
57
  }
29
58
  interface LLMResponse {
30
59
  text: string;
@@ -88,7 +117,9 @@ declare abstract class LLMAgent {
88
117
  * back to `process.env` exactly as before.
89
118
  */
90
119
  protected readonly routeOverride: AgentConfig['routeOverride'];
91
- constructor(routeOverride?: AgentConfig['routeOverride']);
120
+ /** See `AgentConfig.onRetry` (#489). Absent is a no-op. */
121
+ protected readonly onRetry: AgentConfig['onRetry'];
122
+ constructor(routeOverride?: AgentConfig['routeOverride'], onRetry?: AgentConfig['onRetry']);
92
123
  protected abstract resolveModel(model: string): string;
93
124
  protected abstract createClient(): Promise<unknown>;
94
125
  protected getClient<T>(): Promise<T>;
@@ -134,8 +165,34 @@ declare abstract class LLMAgent {
134
165
  */
135
166
  sendToolResult(_results: LLMToolResult[]): Promise<void>;
136
167
  /**
137
- * Execute an API call. No retry — if it fails, it fails.
138
- * Logs the error with provider context and re-throws.
168
+ * Execute an API call, retrying once or twice on HTTP 429
169
+ * (rate-limited) before giving up (#489).
170
+ *
171
+ * Policy: on a 429, honor the provider's `Retry-After` header when
172
+ * present (capped at `RETRY_AFTER_CAP_SEC` — a provider asking for a
173
+ * longer wait is treated as "don't retry", not "wait longer": making
174
+ * the caller sit through 15+ more seconds on a single failed call is
175
+ * a bad experience regardless of what's driving the request);
176
+ * otherwise fall back to exponential backoff with jitter, capped at
177
+ * `DEFAULT_BACKOFF_MAX_MS` per attempt. Stops after
178
+ * `MAX_RETRY_ATTEMPTS` retries OR once the cumulative delay would
179
+ * exceed `MAX_TOTAL_RETRY_DELAY_MS`, whichever comes first. Any
180
+ * non-429 error, or a 429 with no attempts left, is logged and
181
+ * re-thrown immediately — unchanged from the pre-#489 behavior.
182
+ *
183
+ * **Budget scope (final-review correction):** `MAX_TOTAL_RETRY_DELAY_MS`
184
+ * bounds a single `apiCall()` invocation, not a whole generation. A
185
+ * generation issues many sequential `apiCall()` calls (coding turns,
186
+ * tool-result round-trips, eval rounds); under sustained throttling
187
+ * each one can independently add up to `MAX_TOTAL_RETRY_DELAY_MS`, so
188
+ * a generation with k rate-limited calls can add up to roughly `k *
189
+ * MAX_TOTAL_RETRY_DELAY_MS` to how long it holds its caller's
190
+ * resources (e.g. the caller's concurrency slot) — not a flat 20s
191
+ * ceiling. No shared per-generation budget is threaded here by
192
+ * design (accepted ruling, not an oversight): the caller's own
193
+ * queue/admission backstop is what bounds how long other
194
+ * queued work waits, independent of any one generation's retry
195
+ * total.
139
196
  */
140
197
  protected apiCall<T>(fn: () => Promise<T>): Promise<T>;
141
198
  }
@@ -153,4 +210,4 @@ declare function createAgent(provider: AgentConfig['provider']): LLMAgent;
153
210
  declare function createAgent(config: AgentConfig): LLMAgent;
154
211
  declare function callLLM(config: AgentConfig, systemPrompt: string, userPrompt: string, maxTokens?: number): Promise<LLMResponse>;
155
212
 
156
- export { type AgentConfig as A, type LLMResponse as L, createAgent as a, callLLM as c };
213
+ export { type AgentConfig as A, type LLMResponse as L, type ProviderRetryInfo as P, createAgent as a, callLLM as c };
package/dist/patch.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { PatchFn } from './harness/types-public.js';
2
2
  import '@ggui-ai/protocol';
3
3
  import './axes-CzLEMDeB.js';
4
- import './types-public-C36k1q6s.js';
4
+ import './types-public-DIpjC-OA.js';
5
5
  import './types-BOvHNG7K.js';
6
6
  import './llm.js';
7
7
  import './policy.js';
@@ -2,8 +2,19 @@ import { LlmProvider, LlmRoute } from '@ggui-ai/protocol';
2
2
 
3
3
  /**
4
4
  * Provider adapter contract — the narrow surface every concrete LLM
5
- * provider client (Anthropic, Google, OpenAI, OpenRouter) implements
6
- * before the generator wires them in.
5
+ * provider client (Anthropic, Google, OpenAI, OpenRouter, Bedrock)
6
+ * implements.
7
+ *
8
+ * **Real role:** a provider-agnostic one-shot LLM caller for
9
+ * lightweight callers that need a single `complete()` round-trip — NOT
10
+ * a generation harness. The primary consumer is the negotiator's LLM
11
+ * seam (`@ggui-ai/mcp-server`'s `llm-backed-negotiator.ts`, via
12
+ * {@link selectAdapter} from `./providers/index.js`); other lightweight
13
+ * one-shot callers use it the same way. It is explicitly NOT part of
14
+ * the generation dispatch path: `createUiGenerator()` accepts no
15
+ * adapter option, and generation-call 429 retry/backoff lives entirely
16
+ * in `harness/llm-router.ts`'s `apiCall()` — a separate retry story for
17
+ * a separate call shape (an agentic SDK loop, not a single completion).
7
18
  *
8
19
  * The contract is deliberately defined independently of concrete
9
20
  * adapters so:
@@ -11,10 +22,9 @@ import { LlmProvider, LlmRoute } from '@ggui-ai/protocol';
11
22
  * - Adapter implementations can be written against a stable surface —
12
23
  * every concrete adapter MUST pass
13
24
  * {@link providerAdapterContract} or it isn't a `ProviderAdapter`.
14
- * - Downstream callers (`createUiGenerator`, render handlers, retry /
15
- * backoff plumbing) build against ONE structured failure shape
16
- * ({@link ProviderError}) instead of inventing per-provider error
17
- * UX.
25
+ * - Downstream one-shot callers build against ONE structured failure
26
+ * shape ({@link ProviderError}) instead of inventing per-provider
27
+ * error UX.
18
28
  * - The negotiator + cache layers can branch on
19
29
  * `error.kind === 'rate-limited'` etc. without parsing strings.
20
30
  *
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/provider-adapter.ts"],"names":[],"mappings":";AAqPO,SAAS,kBAAkB,IAAA,EAMhB;AAChB,EAAA,MAAM,GAAA,GAAqB;AAAA,IACzB,MAAM,IAAA,CAAK,IAAA;AAAA,IACX,UAAU,IAAA,CAAK,QAAA;AAAA,IACf,SAAS,IAAA,CAAK,OAAA;AAAA,IACd,GAAI,KAAK,MAAA,KAAW,MAAA,GAAY,EAAE,MAAA,EAAQ,IAAA,CAAK,MAAA,EAAO,GAAI,EAAC;AAAA,IAC3D,GAAI,KAAK,aAAA,KAAkB,MAAA,GACvB,EAAE,aAAA,EAAe,IAAA,CAAK,aAAA,EAAc,GACpC;AAAC,GACP;AACA,EAAA,OAAO,GAAA;AACT;AAQO,SAAS,kBAAkB,MAAA,EAAmC;AACnE,EAAA,IAAI,MAAA,KAAW,KAAK,OAAO,cAAA;AAC3B,EAAA,IAAI,MAAA,KAAW,KAAK,OAAO,WAAA;AAC3B,EAAA,IAAI,MAAA,KAAW,KAAK,OAAO,cAAA;AAC3B,EAAA,IAAI,MAAA,IAAU,GAAA,IAAO,MAAA,GAAS,GAAA,EAAK,OAAO,cAAA;AAC1C,EAAA,IAAI,MAAA,IAAU,GAAA,IAAO,MAAA,GAAS,GAAA,EAAK,OAAO,cAAA;AAC1C,EAAA,OAAO,SAAA;AACT;AAcO,SAAS,qBAAA,CACd,UACA,OAAA,EACoB;AACpB,EAAA,IAAI,CAAC,OAAA,CAAQ,MAAA,IAAU,OAAA,CAAQ,MAAA,CAAO,WAAW,CAAA,EAAG;AAClD,IAAA,OAAO;AAAA,MACL,EAAA,EAAI,KAAA;AAAA,MACJ,OAAO,iBAAA,CAAkB;AAAA,QACvB,IAAA,EAAM,gBAAA;AAAA,QACN,QAAA;AAAA,QACA,OAAA,EAAS,GAAG,QAAQ,CAAA,qBAAA;AAAA,OACrB;AAAA,KACH;AAAA,EACF;AACA,EAAA,IAAI,CAAC,QAAQ,KAAA,EAAO,KAAA,IAAS,QAAQ,KAAA,CAAM,KAAA,CAAM,WAAW,CAAA,EAAG;AAC7D,IAAA,OAAO;AAAA,MACL,EAAA,EAAI,KAAA;AAAA,MACJ,OAAO,iBAAA,CAAkB;AAAA,QACvB,IAAA,EAAM,cAAA;AAAA,QACN,QAAA;AAAA,QACA,OAAA,EAAS,GAAG,QAAQ,CAAA,sBAAA;AAAA,OACrB;AAAA,KACH;AAAA,EACF;AACA,EAAA,OAAO,EAAE,IAAI,IAAA,EAAK;AACpB","file":"provider-adapter.js","sourcesContent":["/**\n * Provider adapter contract — the narrow surface every concrete LLM\n * provider client (Anthropic, Google, OpenAI, OpenRouter) implements\n * before the generator wires them in.\n *\n * The contract is deliberately defined independently of concrete\n * adapters so:\n *\n * - Adapter implementations can be written against a stable surface —\n * every concrete adapter MUST pass\n * {@link providerAdapterContract} or it isn't a `ProviderAdapter`.\n * - Downstream callers (`createUiGenerator`, render handlers, retry /\n * backoff plumbing) build against ONE structured failure shape\n * ({@link ProviderError}) instead of inventing per-provider error\n * UX.\n * - The negotiator + cache layers can branch on\n * `error.kind === 'rate-limited'` etc. without parsing strings.\n *\n * **Scope:**\n *\n * - Defines the interface, the response shape, the error shape,\n * and a contract test runner that asserts every adapter\n * satisfies the seam.\n * - Does NOT call any LLM. The contract is structural; this\n * package's own tests exercise the runner against a deterministic\n * fixture adapter (`provider-adapter-mock.test-util.ts`).\n *\n * **Design choices, locked:**\n *\n * - One discriminated `ProviderError.kind` union, not per-provider\n * subclasses. Every provider's raw error funnels through\n * `mapError` into one of these kinds. Callers branch on `kind`.\n * - Every error carries `provider` + `message`; transport-shaped\n * errors carry `status`; backoff-shaped errors carry\n * `retryAfterSec`. Optional fields don't appear when they don't\n * apply (no `status: undefined` noise in serialized logs).\n * - `validateConfig` is synchronous + does NO I/O. It catches the\n * \"operator never configured the provider\" case (missing key,\n * malformed model id) before we attempt a network call.\n * - `complete` is the single-completion seam. Streaming, tool\n * calling, function calling, and harness retry land in higher\n * layers — adapters stay narrow.\n */\nimport type { LlmProvider, LlmRoute } from '@ggui-ai/protocol';\n\n/**\n * One LLM completion request as the adapter sees it. Plain data so\n * tests can construct it without DI.\n *\n * `route` is a discriminated `LlmRoute` — `(provider, model)` typed\n * together so the model id is structurally constrained to its\n * provider's namespace (per the `MODELS` registry in\n * `@ggui-ai/protocol/types/llm-route`). This makes the #22 / #42 bug\n * class — passing a LiteLLM-prefixed `'anthropic/claude-haiku-4-5'`\n * where Anthropic expected the wire id `'claude-haiku-4-5-20251001'` —\n * a compile error at every adapter call site. The adapter reads the\n * wire-canonical string off `request.route.model` and passes it\n * verbatim to the provider SDK; no transformation step exists to\n * forget.\n */\nexport interface ProviderRequest {\n /** API key. Resolved upstream by `@ggui-ai/cli/byok-resolver`. */\n readonly apiKey: string;\n /**\n * Discriminated `(provider, model)` route. `route.model` is the\n * wire-canonical id the provider's API expects — adapters pass it\n * verbatim with no stripping or remapping.\n */\n readonly route: LlmRoute;\n /** System prompt text. Adapter wires it into the provider's slot. */\n readonly systemPrompt: string;\n /** User prompt text. */\n readonly userPrompt: string;\n /**\n * Optional cap on output tokens. Adapters MUST honor when the\n * provider supports it; when absent, adapters use the provider\n * default.\n */\n readonly maxTokens?: number;\n /**\n * Optional cancellation. Adapters MUST forward into the provider\n * SDK's abort surface and reject with `ProviderError{kind:'aborted'}`\n * when fired.\n */\n readonly signal?: AbortSignal;\n}\n\n/**\n * Successful provider response, normalized.\n *\n * Tool-calling output, multi-turn dialogue, structured-output\n * coercion all land in higher layers — this contract intentionally\n * stays narrow: text + usage + finish reason. The harness layer\n * composes adapters into multi-turn loops.\n */\nexport interface ProviderResponse {\n /** The completion text returned by the provider. */\n readonly text: string;\n /** Token usage, per-call. Required — adapters synthesize 0 if the\n * provider didn't report it (and document that in their adapter). */\n readonly usage: {\n readonly inputTokens: number;\n readonly outputTokens: number;\n };\n /**\n * Why the model stopped. Mapped through a normalized union so\n * callers don't have to know each provider's vocabulary.\n *\n * - `'stop'` model emitted EOS / stop sequence\n * - `'length'` hit `maxTokens` or provider context limit\n * - `'content-filter'` provider safety filter intervened\n * - `'other'` anything else (provider-specific reason)\n */\n readonly finishReason: 'stop' | 'length' | 'content-filter' | 'other';\n}\n\n/**\n * The structured failure contract. Every concrete adapter's\n * `mapError` MUST funnel raw provider errors into this shape so\n * downstream code branches on `kind`, not on string parsing.\n *\n * Discriminator + optional carriers:\n *\n * - `'no-credentials'` — no key supplied (pre-flight failure;\n * usually surfaced by `validateConfig`,\n * not `mapError`).\n * - `'unauthorized'` — provider returned 401. Key invalid /\n * revoked / wrong type.\n * - `'forbidden'` — provider returned 403. Quota exhausted,\n * org-level block, model not allowed for\n * this key. (Distinct from 401 because\n * \"rotate the key\" doesn't help.)\n * - `'rate-limited'` — provider returned 429. Carries\n * `retryAfterSec` when the provider\n * advertised one (`Retry-After` header).\n * - `'server-error'` — provider returned 5xx. Retryable.\n * - `'client-error'` — provider returned other 4xx. Usually\n * non-retryable (bad model id, malformed\n * request).\n * - `'network'` — TCP-level failure, DNS, EAI_AGAIN, etc.\n * No `status`. Retryable.\n * - `'invalid-response'` — provider returned 2xx with a body the\n * adapter couldn't parse. Bug in the\n * adapter or in the provider; treat as\n * non-retryable.\n * - `'aborted'` — caller aborted via `signal`.\n * - `'unknown'` — last-resort bucket. Adapters that hit\n * this should be patched to map the\n * specific case explicitly.\n */\nexport type ProviderErrorKind =\n | 'no-credentials'\n | 'unauthorized'\n | 'forbidden'\n | 'rate-limited'\n | 'server-error'\n | 'client-error'\n | 'network'\n | 'invalid-response'\n | 'aborted'\n | 'unknown';\n\nexport interface ProviderError {\n readonly kind: ProviderErrorKind;\n readonly provider: LlmProvider;\n /** Human-readable message. Safe to log; should not contain key\n * material — adapters MUST scrub before placing here. */\n readonly message: string;\n /** HTTP status when transport-shaped. Absent for pre-flight,\n * network, aborted, invalid-response. */\n readonly status?: number;\n /**\n * Seconds to wait before retrying. Set ONLY for `'rate-limited'`\n * when the provider advertised a `Retry-After` header. Higher-level\n * retry policy uses this when present; falls back to its own\n * exponential backoff when absent.\n */\n readonly retryAfterSec?: number;\n}\n\n/**\n * Pre-flight config validation. Synchronous, no I/O. Catches:\n *\n * - missing or empty API key → no-credentials\n * - missing or empty model id → client-error\n *\n * Adapters MAY widen the check with provider-specific rules\n * (e.g. \"openrouter model id MUST contain a `/`\"). Failures\n * surface through the same {@link ProviderError} so callers branch\n * on one union.\n */\nexport type ProviderValidation =\n | { readonly ok: true }\n | { readonly ok: false; readonly error: ProviderError };\n\n/**\n * The contract every provider client implements.\n */\nexport interface ProviderAdapter {\n /** Provider this adapter speaks to. Stable identifier; lets the\n * caller route a `ProviderRequest` to the right adapter. */\n readonly provider: LlmProvider;\n /**\n * Pre-flight check. MUST return synchronously and MUST NOT touch\n * the network. Returns `{ok: true}` when the request is at least\n * structurally valid; returns `{ok: false, error}` otherwise.\n */\n validateConfig(request: Pick<ProviderRequest, 'apiKey' | 'route'>): ProviderValidation;\n /**\n * Single-completion call. Resolves with `{ok: true, response}` on\n * success or `{ok: false, error}` on any failure (network,\n * provider 4xx/5xx, abort). Implementations MUST:\n *\n * 1. Funnel every raw provider error through `mapError`.\n * 2. NOT throw. The result discriminator IS the error path.\n * 3. Forward `request.signal` into the provider SDK's abort\n * mechanism + return `kind:'aborted'` when fired.\n */\n complete(request: ProviderRequest): Promise<ProviderResult>;\n /**\n * Map an arbitrary thrown / rejected value into a\n * {@link ProviderError}. Exposed (not just internal) so test\n * harnesses + retry policies can normalize errors without going\n * through `complete`.\n *\n * MUST NEVER throw — `mapError(unknown)` is the safety net.\n */\n mapError(raw: unknown): ProviderError;\n}\n\n/**\n * Discriminated result of a `complete` call. Mirrors `validateConfig`\n * so callers never have to handle thrown errors from this seam.\n */\nexport type ProviderResult =\n | { readonly ok: true; readonly response: ProviderResponse }\n | { readonly ok: false; readonly error: ProviderError };\n\n// ─── Helpers concrete adapters compose with ────────────────────────\n\n/**\n * Build a `ProviderError` of the supplied kind. Centralized so\n * concrete adapters don't drift on field shape. Provider-aware so\n * callers don't have to thread it through.\n */\nexport function makeProviderError(args: {\n readonly kind: ProviderErrorKind;\n readonly provider: LlmProvider;\n readonly message: string;\n readonly status?: number;\n readonly retryAfterSec?: number;\n}): ProviderError {\n const out: ProviderError = {\n kind: args.kind,\n provider: args.provider,\n message: args.message,\n ...(args.status !== undefined ? { status: args.status } : {}),\n ...(args.retryAfterSec !== undefined\n ? { retryAfterSec: args.retryAfterSec }\n : {}),\n };\n return out;\n}\n\n/**\n * Map an HTTP status code to the canonical error kind. Adapters\n * call this from `mapError` after they've classified the failure as\n * \"the provider returned a status we understand\". Network / abort /\n * pre-flight live outside this mapping.\n */\nexport function statusToErrorKind(status: number): ProviderErrorKind {\n if (status === 401) return 'unauthorized';\n if (status === 403) return 'forbidden';\n if (status === 429) return 'rate-limited';\n if (status >= 500 && status < 600) return 'server-error';\n if (status >= 400 && status < 500) return 'client-error';\n return 'unknown';\n}\n\n/**\n * Default `validateConfig` implementation. Concrete adapters\n * compose this and add their own provider-specific checks on top.\n *\n * Catches the two universal failures: missing key + missing model.\n * Returns `{ok: true}` when both are present.\n *\n * The model-id check is defense-in-depth — typed `LlmRoute` callers\n * cannot construct a route with an empty model string, but the\n * adapter still validates at runtime in case the request crossed an\n * untyped JSON boundary (e.g. deserialized from a wire format).\n */\nexport function defaultValidateConfig(\n provider: LlmProvider,\n request: Pick<ProviderRequest, 'apiKey' | 'route'>,\n): ProviderValidation {\n if (!request.apiKey || request.apiKey.length === 0) {\n return {\n ok: false,\n error: makeProviderError({\n kind: 'no-credentials',\n provider,\n message: `${provider}: no API key supplied`,\n }),\n };\n }\n if (!request.route?.model || request.route.model.length === 0) {\n return {\n ok: false,\n error: makeProviderError({\n kind: 'client-error',\n provider,\n message: `${provider}: model id is required`,\n }),\n };\n }\n return { ok: true };\n}\n"]}
1
+ {"version":3,"sources":["../src/provider-adapter.ts"],"names":[],"mappings":";AA+PO,SAAS,kBAAkB,IAAA,EAMhB;AAChB,EAAA,MAAM,GAAA,GAAqB;AAAA,IACzB,MAAM,IAAA,CAAK,IAAA;AAAA,IACX,UAAU,IAAA,CAAK,QAAA;AAAA,IACf,SAAS,IAAA,CAAK,OAAA;AAAA,IACd,GAAI,KAAK,MAAA,KAAW,MAAA,GAAY,EAAE,MAAA,EAAQ,IAAA,CAAK,MAAA,EAAO,GAAI,EAAC;AAAA,IAC3D,GAAI,KAAK,aAAA,KAAkB,MAAA,GACvB,EAAE,aAAA,EAAe,IAAA,CAAK,aAAA,EAAc,GACpC;AAAC,GACP;AACA,EAAA,OAAO,GAAA;AACT;AAQO,SAAS,kBAAkB,MAAA,EAAmC;AACnE,EAAA,IAAI,MAAA,KAAW,KAAK,OAAO,cAAA;AAC3B,EAAA,IAAI,MAAA,KAAW,KAAK,OAAO,WAAA;AAC3B,EAAA,IAAI,MAAA,KAAW,KAAK,OAAO,cAAA;AAC3B,EAAA,IAAI,MAAA,IAAU,GAAA,IAAO,MAAA,GAAS,GAAA,EAAK,OAAO,cAAA;AAC1C,EAAA,IAAI,MAAA,IAAU,GAAA,IAAO,MAAA,GAAS,GAAA,EAAK,OAAO,cAAA;AAC1C,EAAA,OAAO,SAAA;AACT;AAcO,SAAS,qBAAA,CACd,UACA,OAAA,EACoB;AACpB,EAAA,IAAI,CAAC,OAAA,CAAQ,MAAA,IAAU,OAAA,CAAQ,MAAA,CAAO,WAAW,CAAA,EAAG;AAClD,IAAA,OAAO;AAAA,MACL,EAAA,EAAI,KAAA;AAAA,MACJ,OAAO,iBAAA,CAAkB;AAAA,QACvB,IAAA,EAAM,gBAAA;AAAA,QACN,QAAA;AAAA,QACA,OAAA,EAAS,GAAG,QAAQ,CAAA,qBAAA;AAAA,OACrB;AAAA,KACH;AAAA,EACF;AACA,EAAA,IAAI,CAAC,QAAQ,KAAA,EAAO,KAAA,IAAS,QAAQ,KAAA,CAAM,KAAA,CAAM,WAAW,CAAA,EAAG;AAC7D,IAAA,OAAO;AAAA,MACL,EAAA,EAAI,KAAA;AAAA,MACJ,OAAO,iBAAA,CAAkB;AAAA,QACvB,IAAA,EAAM,cAAA;AAAA,QACN,QAAA;AAAA,QACA,OAAA,EAAS,GAAG,QAAQ,CAAA,sBAAA;AAAA,OACrB;AAAA,KACH;AAAA,EACF;AACA,EAAA,OAAO,EAAE,IAAI,IAAA,EAAK;AACpB","file":"provider-adapter.js","sourcesContent":["/**\n * Provider adapter contract — the narrow surface every concrete LLM\n * provider client (Anthropic, Google, OpenAI, OpenRouter, Bedrock)\n * implements.\n *\n * **Real role:** a provider-agnostic one-shot LLM caller for\n * lightweight callers that need a single `complete()` round-trip — NOT\n * a generation harness. The primary consumer is the negotiator's LLM\n * seam (`@ggui-ai/mcp-server`'s `llm-backed-negotiator.ts`, via\n * {@link selectAdapter} from `./providers/index.js`); other lightweight\n * one-shot callers use it the same way. It is explicitly NOT part of\n * the generation dispatch path: `createUiGenerator()` accepts no\n * adapter option, and generation-call 429 retry/backoff lives entirely\n * in `harness/llm-router.ts`'s `apiCall()` — a separate retry story for\n * a separate call shape (an agentic SDK loop, not a single completion).\n *\n * The contract is deliberately defined independently of concrete\n * adapters so:\n *\n * - Adapter implementations can be written against a stable surface —\n * every concrete adapter MUST pass\n * {@link providerAdapterContract} or it isn't a `ProviderAdapter`.\n * - Downstream one-shot callers build against ONE structured failure\n * shape ({@link ProviderError}) instead of inventing per-provider\n * error UX.\n * - The negotiator + cache layers can branch on\n * `error.kind === 'rate-limited'` etc. without parsing strings.\n *\n * **Scope:**\n *\n * - Defines the interface, the response shape, the error shape,\n * and a contract test runner that asserts every adapter\n * satisfies the seam.\n * - Does NOT call any LLM. The contract is structural; this\n * package's own tests exercise the runner against a deterministic\n * fixture adapter (`provider-adapter-mock.test-util.ts`).\n *\n * **Design choices, locked:**\n *\n * - One discriminated `ProviderError.kind` union, not per-provider\n * subclasses. Every provider's raw error funnels through\n * `mapError` into one of these kinds. Callers branch on `kind`.\n * - Every error carries `provider` + `message`; transport-shaped\n * errors carry `status`; backoff-shaped errors carry\n * `retryAfterSec`. Optional fields don't appear when they don't\n * apply (no `status: undefined` noise in serialized logs).\n * - `validateConfig` is synchronous + does NO I/O. It catches the\n * \"operator never configured the provider\" case (missing key,\n * malformed model id) before we attempt a network call.\n * - `complete` is the single-completion seam. Streaming, tool\n * calling, function calling, and harness retry land in higher\n * layers — adapters stay narrow.\n */\nimport type { LlmProvider, LlmRoute } from '@ggui-ai/protocol';\n\n/**\n * One LLM completion request as the adapter sees it. Plain data so\n * tests can construct it without DI.\n *\n * `route` is a discriminated `LlmRoute` — `(provider, model)` typed\n * together so the model id is structurally constrained to its\n * provider's namespace (per the `MODELS` registry in\n * `@ggui-ai/protocol/types/llm-route`). This makes the #22 / #42 bug\n * class — passing a LiteLLM-prefixed `'anthropic/claude-haiku-4-5'`\n * where Anthropic expected the wire id `'claude-haiku-4-5-20251001'` —\n * a compile error at every adapter call site. The adapter reads the\n * wire-canonical string off `request.route.model` and passes it\n * verbatim to the provider SDK; no transformation step exists to\n * forget.\n */\nexport interface ProviderRequest {\n /** API key. Resolved upstream by `@ggui-ai/cli/byok-resolver`. */\n readonly apiKey: string;\n /**\n * Discriminated `(provider, model)` route. `route.model` is the\n * wire-canonical id the provider's API expects — adapters pass it\n * verbatim with no stripping or remapping.\n */\n readonly route: LlmRoute;\n /** System prompt text. Adapter wires it into the provider's slot. */\n readonly systemPrompt: string;\n /** User prompt text. */\n readonly userPrompt: string;\n /**\n * Optional cap on output tokens. Adapters MUST honor when the\n * provider supports it; when absent, adapters use the provider\n * default.\n */\n readonly maxTokens?: number;\n /**\n * Optional cancellation. Adapters MUST forward into the provider\n * SDK's abort surface and reject with `ProviderError{kind:'aborted'}`\n * when fired.\n */\n readonly signal?: AbortSignal;\n}\n\n/**\n * Successful provider response, normalized.\n *\n * Tool-calling output, multi-turn dialogue, structured-output\n * coercion all land in higher layers — this contract intentionally\n * stays narrow: text + usage + finish reason. The harness layer\n * composes adapters into multi-turn loops.\n */\nexport interface ProviderResponse {\n /** The completion text returned by the provider. */\n readonly text: string;\n /** Token usage, per-call. Required — adapters synthesize 0 if the\n * provider didn't report it (and document that in their adapter). */\n readonly usage: {\n readonly inputTokens: number;\n readonly outputTokens: number;\n };\n /**\n * Why the model stopped. Mapped through a normalized union so\n * callers don't have to know each provider's vocabulary.\n *\n * - `'stop'` model emitted EOS / stop sequence\n * - `'length'` hit `maxTokens` or provider context limit\n * - `'content-filter'` provider safety filter intervened\n * - `'other'` anything else (provider-specific reason)\n */\n readonly finishReason: 'stop' | 'length' | 'content-filter' | 'other';\n}\n\n/**\n * The structured failure contract. Every concrete adapter's\n * `mapError` MUST funnel raw provider errors into this shape so\n * downstream code branches on `kind`, not on string parsing.\n *\n * Discriminator + optional carriers:\n *\n * - `'no-credentials'` — no key supplied (pre-flight failure;\n * usually surfaced by `validateConfig`,\n * not `mapError`).\n * - `'unauthorized'` — provider returned 401. Key invalid /\n * revoked / wrong type.\n * - `'forbidden'` — provider returned 403. Quota exhausted,\n * org-level block, model not allowed for\n * this key. (Distinct from 401 because\n * \"rotate the key\" doesn't help.)\n * - `'rate-limited'` — provider returned 429. Carries\n * `retryAfterSec` when the provider\n * advertised one (`Retry-After` header).\n * - `'server-error'` — provider returned 5xx. Retryable.\n * - `'client-error'` — provider returned other 4xx. Usually\n * non-retryable (bad model id, malformed\n * request).\n * - `'network'` — TCP-level failure, DNS, EAI_AGAIN, etc.\n * No `status`. Retryable.\n * - `'invalid-response'` — provider returned 2xx with a body the\n * adapter couldn't parse. Bug in the\n * adapter or in the provider; treat as\n * non-retryable.\n * - `'aborted'` — caller aborted via `signal`.\n * - `'unknown'` — last-resort bucket. Adapters that hit\n * this should be patched to map the\n * specific case explicitly.\n */\nexport type ProviderErrorKind =\n | 'no-credentials'\n | 'unauthorized'\n | 'forbidden'\n | 'rate-limited'\n | 'server-error'\n | 'client-error'\n | 'network'\n | 'invalid-response'\n | 'aborted'\n | 'unknown';\n\nexport interface ProviderError {\n readonly kind: ProviderErrorKind;\n readonly provider: LlmProvider;\n /** Human-readable message. Safe to log; should not contain key\n * material — adapters MUST scrub before placing here. */\n readonly message: string;\n /** HTTP status when transport-shaped. Absent for pre-flight,\n * network, aborted, invalid-response. */\n readonly status?: number;\n /**\n * Seconds to wait before retrying. Set ONLY for `'rate-limited'`\n * when the provider advertised a `Retry-After` header. Higher-level\n * retry policy uses this when present; falls back to its own\n * exponential backoff when absent.\n */\n readonly retryAfterSec?: number;\n}\n\n/**\n * Pre-flight config validation. Synchronous, no I/O. Catches:\n *\n * - missing or empty API key → no-credentials\n * - missing or empty model id → client-error\n *\n * Adapters MAY widen the check with provider-specific rules\n * (e.g. \"openrouter model id MUST contain a `/`\"). Failures\n * surface through the same {@link ProviderError} so callers branch\n * on one union.\n */\nexport type ProviderValidation =\n | { readonly ok: true }\n | { readonly ok: false; readonly error: ProviderError };\n\n/**\n * The contract every provider client implements.\n */\nexport interface ProviderAdapter {\n /** Provider this adapter speaks to. Stable identifier; lets the\n * caller route a `ProviderRequest` to the right adapter. */\n readonly provider: LlmProvider;\n /**\n * Pre-flight check. MUST return synchronously and MUST NOT touch\n * the network. Returns `{ok: true}` when the request is at least\n * structurally valid; returns `{ok: false, error}` otherwise.\n */\n validateConfig(request: Pick<ProviderRequest, 'apiKey' | 'route'>): ProviderValidation;\n /**\n * Single-completion call. Resolves with `{ok: true, response}` on\n * success or `{ok: false, error}` on any failure (network,\n * provider 4xx/5xx, abort). Implementations MUST:\n *\n * 1. Funnel every raw provider error through `mapError`.\n * 2. NOT throw. The result discriminator IS the error path.\n * 3. Forward `request.signal` into the provider SDK's abort\n * mechanism + return `kind:'aborted'` when fired.\n */\n complete(request: ProviderRequest): Promise<ProviderResult>;\n /**\n * Map an arbitrary thrown / rejected value into a\n * {@link ProviderError}. Exposed (not just internal) so test\n * harnesses + retry policies can normalize errors without going\n * through `complete`.\n *\n * MUST NEVER throw — `mapError(unknown)` is the safety net.\n */\n mapError(raw: unknown): ProviderError;\n}\n\n/**\n * Discriminated result of a `complete` call. Mirrors `validateConfig`\n * so callers never have to handle thrown errors from this seam.\n */\nexport type ProviderResult =\n | { readonly ok: true; readonly response: ProviderResponse }\n | { readonly ok: false; readonly error: ProviderError };\n\n// ─── Helpers concrete adapters compose with ────────────────────────\n\n/**\n * Build a `ProviderError` of the supplied kind. Centralized so\n * concrete adapters don't drift on field shape. Provider-aware so\n * callers don't have to thread it through.\n */\nexport function makeProviderError(args: {\n readonly kind: ProviderErrorKind;\n readonly provider: LlmProvider;\n readonly message: string;\n readonly status?: number;\n readonly retryAfterSec?: number;\n}): ProviderError {\n const out: ProviderError = {\n kind: args.kind,\n provider: args.provider,\n message: args.message,\n ...(args.status !== undefined ? { status: args.status } : {}),\n ...(args.retryAfterSec !== undefined\n ? { retryAfterSec: args.retryAfterSec }\n : {}),\n };\n return out;\n}\n\n/**\n * Map an HTTP status code to the canonical error kind. Adapters\n * call this from `mapError` after they've classified the failure as\n * \"the provider returned a status we understand\". Network / abort /\n * pre-flight live outside this mapping.\n */\nexport function statusToErrorKind(status: number): ProviderErrorKind {\n if (status === 401) return 'unauthorized';\n if (status === 403) return 'forbidden';\n if (status === 429) return 'rate-limited';\n if (status >= 500 && status < 600) return 'server-error';\n if (status >= 400 && status < 500) return 'client-error';\n return 'unknown';\n}\n\n/**\n * Default `validateConfig` implementation. Concrete adapters\n * compose this and add their own provider-specific checks on top.\n *\n * Catches the two universal failures: missing key + missing model.\n * Returns `{ok: true}` when both are present.\n *\n * The model-id check is defense-in-depth — typed `LlmRoute` callers\n * cannot construct a route with an empty model string, but the\n * adapter still validates at runtime in case the request crossed an\n * untyped JSON boundary (e.g. deserialized from a wire format).\n */\nexport function defaultValidateConfig(\n provider: LlmProvider,\n request: Pick<ProviderRequest, 'apiKey' | 'route'>,\n): ProviderValidation {\n if (!request.apiKey || request.apiKey.length === 0) {\n return {\n ok: false,\n error: makeProviderError({\n kind: 'no-credentials',\n provider,\n message: `${provider}: no API key supplied`,\n }),\n };\n }\n if (!request.route?.model || request.route.model.length === 0) {\n return {\n ok: false,\n error: makeProviderError({\n kind: 'client-error',\n provider,\n message: `${provider}: model id is required`,\n }),\n };\n }\n return { ok: true };\n}\n"]}
@@ -134,8 +134,9 @@ interface BedrockAdapterOptions {
134
134
  *
135
135
  * No API key — IAM is the auth boundary. The returned `ProviderAdapter`
136
136
  * satisfies the same contract as `createAnthropicAdapter`, so it
137
- * slots into `createUiGenerator({ adapter })` interchangeably (modulo
138
- * the per-provider model-id namespace differences).
137
+ * slots into any one-shot `ProviderAdapter` caller (e.g. `selectAdapter`)
138
+ * interchangeably (modulo the per-provider model-id namespace
139
+ * differences).
139
140
  */
140
141
  declare function createBedrockAdapter(options?: BedrockAdapterOptions): ProviderAdapter;
141
142
 
@@ -146,6 +147,15 @@ declare function createBedrockAdapter(options?: BedrockAdapterOptions): Provider
146
147
  * providers ggui supports today: Anthropic (direct API), Google,
147
148
  * OpenAI, OpenRouter, and AWS Bedrock (Anthropic models via IAM).
148
149
  *
150
+ * **Real role:** a provider-agnostic one-shot LLM caller for
151
+ * lightweight callers that need a single `complete()` round-trip — NOT
152
+ * the generation dispatch path. `createUiGenerator()` accepts no
153
+ * adapter option; generation-call retry/backoff lives entirely in
154
+ * `harness/llm-router.ts`'s `apiCall()`. The primary consumer is the
155
+ * negotiator's LLM seam (`@ggui-ai/mcp-server`'s
156
+ * `llm-backed-negotiator.ts`, via {@link selectAdapter}); other
157
+ * lightweight one-shot callers use it the same way.
158
+ *
149
159
  * Every adapter satisfies the structural
150
160
  * {@link import('../provider-adapter.js').ProviderAdapter} contract.
151
161
  * The four direct-API adapters compose
@@ -156,15 +166,18 @@ declare function createBedrockAdapter(options?: BedrockAdapterOptions): Provider
156
166
  * because rolling our own AWS SigV4 signer for one provider isn't a
157
167
  * reasonable trade.
158
168
  *
159
- * Typical usage from the CLI:
169
+ * Typical usage — a one-shot completion:
160
170
  *
161
171
  * ```ts
162
172
  * import { createAnthropicAdapter } from '@ggui-ai/ui-gen/providers';
163
- * import { createUiGenerator } from '@ggui-ai/ui-gen';
164
173
  *
165
174
  * const adapter = createAnthropicAdapter();
166
- * const generator = createUiGenerator({ adapter });
167
- * const result = await generator.generate({ request, llm, providerKey, blueprints });
175
+ * const result = await adapter.complete({
176
+ * apiKey,
177
+ * route: { provider: 'anthropic', model: 'claude-haiku-4-5-20251001' },
178
+ * systemPrompt,
179
+ * userPrompt,
180
+ * });
168
181
  * ```
169
182
  *
170
183
  * Bedrock pool path (no API key — IAM at process boot):