@ggui-ai/ui-gen 0.8.0 → 0.10.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.
- package/dist/adapters/generation-dispatch.d.ts +18 -1
- package/dist/adapters/generation-dispatch.js +819 -1577
- package/dist/adapters/generation-dispatch.js.map +1 -1
- package/dist/adapters/index.js +28 -11
- package/dist/adapters/index.js.map +1 -1
- package/dist/advanced/index.d.ts +5 -3
- package/dist/advanced/index.js +838 -1579
- package/dist/advanced/index.js.map +1 -1
- package/dist/blueprint-validator.js +109 -3
- package/dist/blueprint-validator.js.map +1 -1
- package/dist/boilerplate.js +17 -4
- package/dist/boilerplate.js.map +1 -1
- package/dist/check/index.d.ts +1 -1
- package/dist/check/index.js +11 -1
- package/dist/check/index.js.map +1 -1
- package/dist/compose.js +5 -5
- package/dist/compose.js.map +1 -1
- package/dist/{create-ui-generator-GX7tvbyH.d.ts → create-ui-generator-CF0EL0Li.d.ts} +26 -1
- package/dist/{dispatch-Deo9qzrG.d.ts → dispatch-D75M-OfZ.d.ts} +1 -1
- package/dist/evaluation/axis-checks/index.d.ts +3 -3
- package/dist/evaluation/axis-checks/index.js +133 -1
- package/dist/evaluation/axis-checks/index.js.map +1 -1
- package/dist/evaluation/axis-checks/registry.d.ts +1 -1
- package/dist/evaluation/axis-checks/registry.js +133 -1
- package/dist/evaluation/axis-checks/registry.js.map +1 -1
- package/dist/evaluation/index.d.ts +46 -3
- package/dist/evaluation/index.js +410 -24
- package/dist/evaluation/index.js.map +1 -1
- package/dist/fragments/index.js +5 -5
- package/dist/fragments/index.js.map +1 -1
- package/dist/harness/check/runtime-render/index.d.ts +2 -2
- package/dist/harness/check/runtime-render/index.js +110 -3
- package/dist/harness/check/runtime-render/index.js.map +1 -1
- package/dist/harness/index.d.ts +5 -126
- package/dist/harness/index.js +292 -32
- package/dist/harness/index.js.map +1 -1
- package/dist/harness/llm-trace-sink.d.ts +1 -1
- package/dist/harness/llm-trace-sink.js.map +1 -1
- package/dist/harness/prompts.js +17 -3
- package/dist/harness/prompts.js.map +1 -1
- package/dist/harness/result-types.d.ts +1 -1
- package/dist/harness/runtime.d.ts +17 -1
- package/dist/harness/runtime.js +23 -8
- package/dist/harness/runtime.js.map +1 -1
- package/dist/harness/types-public.d.ts +1 -1
- package/dist/harness/types-public.js +22 -9
- package/dist/harness/types-public.js.map +1 -1
- package/dist/harness/validator-trace-sink.d.ts +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +838 -1579
- package/dist/index.js.map +1 -1
- package/dist/llm-router-BEVtbAb9.d.ts +213 -0
- package/dist/patch.d.ts +1 -1
- package/dist/provider-adapter.d.ts +16 -6
- package/dist/provider-adapter.js.map +1 -1
- package/dist/providers/index.d.ts +54 -99
- package/dist/providers/index.js +13 -8
- package/dist/providers/index.js.map +1 -1
- package/dist/{render-check-CNZjHD5g.d.ts → render-check-BNYGVDJd.d.ts} +1 -1
- package/dist/{types-public-C36k1q6s.d.ts → types-public-DIpjC-OA.d.ts} +1 -1
- package/dist/validation/index.js +2 -2
- package/dist/validation/index.js.map +1 -1
- package/dist/workflows.d.ts +1 -1
- package/package.json +18 -18
- package/src/adapters/generation-dispatch.ts +28 -2
- package/src/adapters/openrouter/client.ts +2 -2
- package/src/adapters/openrouter/types.ts +10 -0
- package/src/boilerplate/generate.ts +10 -2
- package/src/boilerplate/json-schema-ts.ts +22 -2
- package/src/boilerplate/system-prompt.ts +4 -2
- package/src/compile.ts +1 -1
- package/src/contract-context.ts +10 -2
- package/src/create-ui-generator.ts +58 -11
- package/src/design-system-docs.ts +6 -4
- package/src/evaluation/axis-checks/checks/state-merge.ts +1 -1
- package/src/evaluation/axis-checks/checks/state-ui-affordance.ts +162 -0
- package/src/evaluation/axis-checks/checks/universal.ts +39 -0
- package/src/evaluation/axis-checks/registry.ts +2 -0
- package/src/evaluation/index.ts +11 -0
- package/src/evaluation/llm-evaluator.ts +26 -3
- package/src/evaluation/visual-evaluator.ts +49 -66
- package/src/fragments/realtime.ts +3 -3
- package/src/fragments/state.ts +2 -2
- package/src/harness/check/runtime-render/adapter.ts +18 -0
- package/src/harness/check/runtime-render/index.ts +8 -0
- package/src/harness/check/runtime-render/prepare-mockup.ts +14 -0
- package/src/harness/check/runtime-render/production-act-shim.ts +158 -0
- package/src/harness/check/runtime-render/render-check.ts +72 -1
- package/src/harness/coding/init-session.ts +4 -1
- package/src/harness/coding/run-eval-round.ts +67 -5
- package/src/harness/index.ts +1 -1
- package/src/harness/llm-router.ts +451 -27
- package/src/harness/llm-trace-sink.ts +2 -1
- package/src/harness/runtime.ts +15 -0
- package/src/index.ts +5 -5
- package/src/provider-adapter.ts +16 -6
- package/src/providers/bedrock.ts +89 -26
- package/src/providers/index.ts +16 -4
- package/src/tools/render-check.ts +19 -7
- package/src/validation/ui-compiler.ts +2 -2
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { JsonObject } from '@ggui-ai/protocol';
|
|
2
|
+
import { LLMToolDef } from './llm.js';
|
|
3
|
+
|
|
4
|
+
interface AgentConfig {
|
|
5
|
+
provider: 'anthropic' | 'openai' | 'google' | 'openrouter';
|
|
6
|
+
model: string;
|
|
7
|
+
/**
|
|
8
|
+
* Sampling temperature. When defined it is threaded into the provider
|
|
9
|
+
* request; when undefined the provider default is used (unchanged
|
|
10
|
+
* behavior for all existing callers).
|
|
11
|
+
*/
|
|
12
|
+
temperature?: number;
|
|
13
|
+
/**
|
|
14
|
+
* Explicit per-call credentials/routing, bypassing `process.env`
|
|
15
|
+
* entirely when supplied. Absent (default) preserves today's
|
|
16
|
+
* behavior — each agent falls back to reading its provider's env
|
|
17
|
+
* var(s) directly, so existing callers (including self-hosters who
|
|
18
|
+
* set env vars once at process start) are unaffected. Use this to
|
|
19
|
+
* run concurrent generations safely in one process: two simultaneous
|
|
20
|
+
* calls with different `routeOverride`s never race on shared
|
|
21
|
+
* `process.env` state.
|
|
22
|
+
*/
|
|
23
|
+
routeOverride?: {
|
|
24
|
+
readonly apiKey?: string;
|
|
25
|
+
/** Anthropic-only: force the Bedrock-IAM client instead of the direct API client. */
|
|
26
|
+
readonly useBedrock?: boolean;
|
|
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;
|
|
57
|
+
}
|
|
58
|
+
interface LLMResponse {
|
|
59
|
+
text: string;
|
|
60
|
+
inputTokens: number;
|
|
61
|
+
outputTokens: number;
|
|
62
|
+
}
|
|
63
|
+
interface LLMTool {
|
|
64
|
+
name: string;
|
|
65
|
+
description: string;
|
|
66
|
+
parameters: JsonObject;
|
|
67
|
+
handler: (args: JsonObject) => Promise<{
|
|
68
|
+
content: Array<{
|
|
69
|
+
text: string;
|
|
70
|
+
}>;
|
|
71
|
+
isError?: boolean;
|
|
72
|
+
}>;
|
|
73
|
+
}
|
|
74
|
+
interface LLMWithToolsResponse {
|
|
75
|
+
text: string;
|
|
76
|
+
inputTokens: number;
|
|
77
|
+
outputTokens: number;
|
|
78
|
+
turnsUsed: number;
|
|
79
|
+
}
|
|
80
|
+
interface LLMToolCall {
|
|
81
|
+
/** Provider-specific call ID (for sendToolResult) */
|
|
82
|
+
id?: string;
|
|
83
|
+
name: string;
|
|
84
|
+
input: JsonObject;
|
|
85
|
+
}
|
|
86
|
+
interface LLMToolCallResponse {
|
|
87
|
+
toolCalls: LLMToolCall[];
|
|
88
|
+
inputTokens: number;
|
|
89
|
+
outputTokens: number;
|
|
90
|
+
/**
|
|
91
|
+
* Tokens read from / written to the prompt cache, when the provider
|
|
92
|
+
* reports them. Optional because not every provider exposes
|
|
93
|
+
* prompt-cache accounting — absent means "unreported", never zero.
|
|
94
|
+
*/
|
|
95
|
+
cacheReadTokens?: number;
|
|
96
|
+
cacheCreationTokens?: number;
|
|
97
|
+
}
|
|
98
|
+
/** Result of executing a tool — passed to sendToolResult to close the API contract. */
|
|
99
|
+
interface LLMToolResult {
|
|
100
|
+
/** Tool call ID from the response (for providers that need it) */
|
|
101
|
+
callId?: string;
|
|
102
|
+
/** Tool name */
|
|
103
|
+
name: string;
|
|
104
|
+
/** Text result of executing the tool */
|
|
105
|
+
result: string;
|
|
106
|
+
/** Whether the tool execution failed */
|
|
107
|
+
isError?: boolean;
|
|
108
|
+
}
|
|
109
|
+
declare abstract class LLMAgent {
|
|
110
|
+
abstract readonly provider: AgentConfig['provider'];
|
|
111
|
+
private client;
|
|
112
|
+
protected lastSessionId: string | undefined;
|
|
113
|
+
/**
|
|
114
|
+
* Explicit per-call routing override — see `AgentConfig.routeOverride`.
|
|
115
|
+
* Absent when constructed via the bare `new XAgent()` form (every
|
|
116
|
+
* existing call site), so `createClient()`/`resolveModel()` fall
|
|
117
|
+
* back to `process.env` exactly as before.
|
|
118
|
+
*/
|
|
119
|
+
protected readonly 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']);
|
|
123
|
+
protected abstract resolveModel(model: string): string;
|
|
124
|
+
protected abstract createClient(): Promise<unknown>;
|
|
125
|
+
protected getClient<T>(): Promise<T>;
|
|
126
|
+
/** Text-only call — no tools */
|
|
127
|
+
abstract callText(model: string, systemPrompt: string, userPrompt: string, maxTokens?: number, temperature?: number): Promise<LLMResponse>;
|
|
128
|
+
/**
|
|
129
|
+
* Single-turn function calling — returns tool calls without executing them.
|
|
130
|
+
* Each provider uses its native function/tool calling:
|
|
131
|
+
* - Anthropic: tool_use blocks
|
|
132
|
+
* - OpenAI: function_call output items
|
|
133
|
+
* - Google: functionCall parts
|
|
134
|
+
* SDK handles JSON escaping — safe for code, diffs, and other content.
|
|
135
|
+
*/
|
|
136
|
+
abstract callTools(model: string, systemPrompt: string, userPrompt: string, tools: LLMToolDef[], toolChoice?: 'required' | 'auto',
|
|
137
|
+
/**
|
|
138
|
+
* Optional scoped fallback tools. If the primary tools fail with a
|
|
139
|
+
* transport-class error (e.g. `malformed_tool_call` on Gemini after
|
|
140
|
+
* retry exhaustion), the provider may retry once with these narrower
|
|
141
|
+
* tools before throwing. Universal signal — not provider-gated.
|
|
142
|
+
*/
|
|
143
|
+
scopedTools?: LLMToolDef[]): Promise<LLMToolCallResponse>;
|
|
144
|
+
/** Multi-turn agentic loop — executes tools internally */
|
|
145
|
+
abstract callWithTools(model: string, systemPrompt: string, userPrompt: string, tools: LLMTool[], maxTurns: number): Promise<LLMWithToolsResponse>;
|
|
146
|
+
/**
|
|
147
|
+
* Pre-warm cache for repeated callTools() calls with the same system prompt + tools.
|
|
148
|
+
* Override in providers that support server-side context caching (e.g., Google).
|
|
149
|
+
* No-op by default (Anthropic/OpenAI handle caching automatically per-request).
|
|
150
|
+
*/
|
|
151
|
+
warmCache(_model: string, _systemPrompt: string, _tools: LLMToolDef[], _toolChoice?: 'required' | 'auto'): Promise<void>;
|
|
152
|
+
/** Cleanup any cached resources. Call after generation completes. No-op by default. */
|
|
153
|
+
cleanup(): Promise<void>;
|
|
154
|
+
/** Reset session state between independent generation runs. */
|
|
155
|
+
resetSession(): void;
|
|
156
|
+
/**
|
|
157
|
+
* Send tool execution results back to the provider to close the API contract.
|
|
158
|
+
* Call this after executing tools from callTools() and before the next callTools().
|
|
159
|
+
*
|
|
160
|
+
* For providers with server-side state (Google, OpenAI), this sends the
|
|
161
|
+
* function results so the next callTools() can chain properly.
|
|
162
|
+
* For stateless providers (Anthropic), this is a no-op.
|
|
163
|
+
*
|
|
164
|
+
* Override in providers that need it.
|
|
165
|
+
*/
|
|
166
|
+
sendToolResult(_results: LLMToolResult[]): Promise<void>;
|
|
167
|
+
/**
|
|
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.
|
|
196
|
+
*/
|
|
197
|
+
protected apiCall<T>(fn: () => Promise<T>): Promise<T>;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Create a fresh agent instance for the given provider.
|
|
201
|
+
* Each call returns a new instance — no shared state between callers.
|
|
202
|
+
*
|
|
203
|
+
* Accepts either a bare provider string (every pre-existing call
|
|
204
|
+
* site — the agent falls back to reading its provider's env var(s)
|
|
205
|
+
* directly) or a full {@link AgentConfig}, whose `routeOverride`
|
|
206
|
+
* (when present) is threaded into the constructed agent so it never
|
|
207
|
+
* touches `process.env`.
|
|
208
|
+
*/
|
|
209
|
+
declare function createAgent(provider: AgentConfig['provider']): LLMAgent;
|
|
210
|
+
declare function createAgent(config: AgentConfig): LLMAgent;
|
|
211
|
+
declare function callLLM(config: AgentConfig, systemPrompt: string, userPrompt: string, maxTokens?: number): Promise<LLMResponse>;
|
|
212
|
+
|
|
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-
|
|
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)
|
|
6
|
-
*
|
|
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
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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"]}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { ProviderAdapter } from '../provider-adapter.js';
|
|
2
|
-
import { AnthropicBedrock } from '@anthropic-ai/bedrock-sdk';
|
|
3
2
|
import { LlmProvider } from '@ggui-ai/mcp-server-core';
|
|
4
3
|
import '@ggui-ai/protocol';
|
|
5
4
|
|
|
@@ -72,95 +71,36 @@ interface OpenRouterAdapterOptions {
|
|
|
72
71
|
declare function createOpenRouterAdapter(options?: OpenRouterAdapterOptions): ProviderAdapter;
|
|
73
72
|
|
|
74
73
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
* chain (IRSA pod token / `~/.aws/credentials` / env vars) supplies
|
|
79
|
-
* SigV4 signatures automatically.
|
|
80
|
-
*
|
|
81
|
-
* ## Why this adapter exists
|
|
82
|
-
*
|
|
83
|
-
* Anyone running the generator on an AWS-credentialed host (EC2, ECS,
|
|
84
|
-
* Lambda, EKS) can target Bedrock without managing API keys: IAM is
|
|
85
|
-
* the auth boundary, AWS rotates host credentials automatically, and
|
|
86
|
-
* a misconfigured IAM role surfaces as a clear `AccessDeniedException`
|
|
87
|
-
* the SDK funnels through `mapError`. The alternative — parking a
|
|
88
|
-
* provider API key in a secrets store and lazily fetching it on first
|
|
89
|
-
* render — works but adds operational surface (a seed ceremony,
|
|
90
|
-
* key-rotation discipline, a misconfig mode where the secret is
|
|
91
|
-
* empty); Bedrock removes all of it.
|
|
92
|
-
*
|
|
93
|
-
* ## Wire shape
|
|
94
|
-
*
|
|
95
|
-
* The Bedrock SDK's `client.messages.create(...)` mirrors the direct
|
|
96
|
-
* Anthropic API surface 1:1 — same request body fields (`model`,
|
|
97
|
-
* `max_tokens`, `system`, `messages`), same response shape (`content[]`
|
|
98
|
-
* with `{type:'text', text}` blocks, `stop_reason`, `usage`). Both
|
|
99
|
-
* adapters therefore share ONE success-envelope parser —
|
|
100
|
-
* {@link parseAnthropicMessagesResponse} in `./anthropic-wire.ts`.
|
|
101
|
-
* Streaming is supported by the SDK
|
|
102
|
-
* (`client.messages.stream(...)` returns an async iterable) but this
|
|
103
|
-
* adapter uses the non-streaming `create(...)` call to match the
|
|
104
|
-
* single-completion {@link ProviderAdapter} contract; higher layers
|
|
105
|
-
* (`UiGenerator`) compose multi-turn loops above the seam.
|
|
106
|
-
*
|
|
107
|
-
* ## Auth — no API key in `ProviderRequest`
|
|
108
|
-
*
|
|
109
|
-
* The {@link ProviderAdapter} contract types `ProviderRequest.apiKey`
|
|
110
|
-
* as a required string because direct API providers need it on the
|
|
111
|
-
* wire. Bedrock doesn't — the SDK signs requests with AWS credentials
|
|
112
|
-
* resolved at process boot. Two compatible options were considered:
|
|
113
|
-
*
|
|
114
|
-
* 1. Add an `auth: 'apiKey' | 'iam'` discriminator to
|
|
115
|
-
* `ProviderAdapter` + thread it through every adapter.
|
|
116
|
-
* 2. Override `validateConfig` so this adapter accepts (and ignores)
|
|
117
|
-
* whatever the caller puts in `apiKey` — `'iam'` / sentinel /
|
|
118
|
-
* empty string all pass.
|
|
119
|
-
*
|
|
120
|
-
* Option 2 wins on cost: ZERO callers, contract, or tests change.
|
|
121
|
-
* Callers pass a sentinel (e.g. `'bedrock-iam'`) so the model-id
|
|
122
|
-
* check still gates on a non-empty value. Future work could add the
|
|
123
|
-
* discriminator if a third auth mode (e.g. cross-account assume-role)
|
|
124
|
-
* lands.
|
|
125
|
-
*
|
|
126
|
-
* ## Model IDs — pass-through
|
|
127
|
-
*
|
|
128
|
-
* Bedrock and the direct Anthropic API use OVERLAPPING but DISTINCT
|
|
129
|
-
* model id namespaces:
|
|
130
|
-
*
|
|
131
|
-
* - Direct API: `claude-haiku-4-5`, `claude-opus-4-7`, etc.
|
|
132
|
-
* - Bedrock foundation models: `anthropic.claude-sonnet-4-6`
|
|
133
|
-
* - Bedrock cross-region inference profiles: `us.anthropic.claude-haiku-4-5-20251001-v1:0`
|
|
134
|
-
*
|
|
135
|
-
* The adapter passes whatever `request.model` contains straight to
|
|
136
|
-
* Bedrock — translation lives in the caller (model picker / deployment
|
|
137
|
-
* config). Supply a Bedrock foundation-model or inference-profile id,
|
|
138
|
-
* not a direct-API model name.
|
|
139
|
-
*
|
|
140
|
-
* ## Failure mapping
|
|
141
|
-
*
|
|
142
|
-
* The bedrock-sdk throws `APIError` subclasses (`AuthenticationError`,
|
|
143
|
-
* `PermissionDeniedError`, `RateLimitError`, `InternalServerError`,
|
|
144
|
-
* etc.) that all carry a numeric `.status` property. We classify them
|
|
145
|
-
* by structural duck-typing (`typeof err.status === 'number'`) rather
|
|
146
|
-
* than `instanceof Anthropic.APIError` because pnpm hoisting often
|
|
147
|
-
* resolves multiple `@anthropic-ai/sdk` versions across the workspace
|
|
148
|
-
* — the bedrock-sdk's nested SDK and ui-gen's direct SDK can diverge,
|
|
149
|
-
* and `instanceof` then silently returns false for valid errors.
|
|
150
|
-
* Duck-typing on `.status` is robust to that drift and matches the
|
|
151
|
-
* SDK's documented API contract (every subclass exposes `.status`).
|
|
152
|
-
*
|
|
153
|
-
* Transport-shaped errors (`APIConnectionError`, `APIUserAbortError`)
|
|
154
|
-
* carry NO status — they fall through to {@link classifyFetchError}
|
|
155
|
-
* for the standard `network` / `aborted` mapping.
|
|
156
|
-
*
|
|
157
|
-
* AWS-specific failure modes (`AccessDeniedException` — wrong IAM
|
|
158
|
-
* grants; `ThrottlingException` — Bedrock rate limit;
|
|
159
|
-
* `ValidationException` — bad model id) all surface as `APIError`
|
|
160
|
-
* subclasses in the SDK, distinguished by `.status` (403 / 429 / 400
|
|
161
|
-
* respectively).
|
|
74
|
+
* Which Bedrock endpoint a model id resolves to. The namespaces are
|
|
75
|
+
* disjoint (see the docstring above), so the shape of the id fully
|
|
76
|
+
* determines the endpoint — no configuration knob needed.
|
|
162
77
|
*/
|
|
163
|
-
|
|
78
|
+
type BedrockEndpoint = 'runtime' | 'mantle';
|
|
79
|
+
/**
|
|
80
|
+
* Structural slice of the two SDK clients the adapter actually drives —
|
|
81
|
+
* the seam that lets tests inject a stub without standing up a real
|
|
82
|
+
* AWS client. Both `AnthropicBedrock` and `AnthropicBedrockMantle`
|
|
83
|
+
* satisfy it (the adapter's single-completion contract only ever calls
|
|
84
|
+
* non-streaming `messages.create`; the response is parsed from
|
|
85
|
+
* `unknown` by `parseAnthropicMessagesResponse`, so the SDK's
|
|
86
|
+
* version-volatile `Message` type is deliberately not part of the
|
|
87
|
+
* seam).
|
|
88
|
+
*/
|
|
89
|
+
interface BedrockMessagesClient {
|
|
90
|
+
readonly messages: {
|
|
91
|
+
create(body: {
|
|
92
|
+
model: string;
|
|
93
|
+
max_tokens: number;
|
|
94
|
+
system?: string;
|
|
95
|
+
messages: Array<{
|
|
96
|
+
role: 'user';
|
|
97
|
+
content: string;
|
|
98
|
+
}>;
|
|
99
|
+
}, options?: {
|
|
100
|
+
signal?: AbortSignal;
|
|
101
|
+
}): Promise<unknown>;
|
|
102
|
+
};
|
|
103
|
+
}
|
|
164
104
|
/**
|
|
165
105
|
* Constructor options for the Bedrock adapter.
|
|
166
106
|
*
|
|
@@ -181,19 +121,22 @@ interface BedrockAdapterOptions {
|
|
|
181
121
|
readonly region?: string;
|
|
182
122
|
/**
|
|
183
123
|
* Optional client factory override — used by tests to inject a
|
|
184
|
-
* mock or stub
|
|
185
|
-
*
|
|
186
|
-
*
|
|
124
|
+
* mock or stub client without actually hitting AWS. Called with the
|
|
125
|
+
* endpoint the request's model id routed to (`'runtime'` →
|
|
126
|
+
* `AnthropicBedrock`, `'mantle'` → `AnthropicBedrockMantle`).
|
|
127
|
+
* Production callers leave this unset; the adapter constructs the
|
|
128
|
+
* real client lazily on first `complete(...)` call per endpoint.
|
|
187
129
|
*/
|
|
188
|
-
readonly clientFactory?: (region: string) =>
|
|
130
|
+
readonly clientFactory?: (region: string, endpoint: BedrockEndpoint) => BedrockMessagesClient;
|
|
189
131
|
}
|
|
190
132
|
/**
|
|
191
133
|
* Construct an AWS Bedrock provider adapter.
|
|
192
134
|
*
|
|
193
135
|
* No API key — IAM is the auth boundary. The returned `ProviderAdapter`
|
|
194
136
|
* satisfies the same contract as `createAnthropicAdapter`, so it
|
|
195
|
-
* slots into
|
|
196
|
-
* the per-provider model-id namespace
|
|
137
|
+
* slots into any one-shot `ProviderAdapter` caller (e.g. `selectAdapter`)
|
|
138
|
+
* interchangeably (modulo the per-provider model-id namespace
|
|
139
|
+
* differences).
|
|
197
140
|
*/
|
|
198
141
|
declare function createBedrockAdapter(options?: BedrockAdapterOptions): ProviderAdapter;
|
|
199
142
|
|
|
@@ -204,6 +147,15 @@ declare function createBedrockAdapter(options?: BedrockAdapterOptions): Provider
|
|
|
204
147
|
* providers ggui supports today: Anthropic (direct API), Google,
|
|
205
148
|
* OpenAI, OpenRouter, and AWS Bedrock (Anthropic models via IAM).
|
|
206
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
|
+
*
|
|
207
159
|
* Every adapter satisfies the structural
|
|
208
160
|
* {@link import('../provider-adapter.js').ProviderAdapter} contract.
|
|
209
161
|
* The four direct-API adapters compose
|
|
@@ -214,15 +166,18 @@ declare function createBedrockAdapter(options?: BedrockAdapterOptions): Provider
|
|
|
214
166
|
* because rolling our own AWS SigV4 signer for one provider isn't a
|
|
215
167
|
* reasonable trade.
|
|
216
168
|
*
|
|
217
|
-
* Typical usage
|
|
169
|
+
* Typical usage — a one-shot completion:
|
|
218
170
|
*
|
|
219
171
|
* ```ts
|
|
220
172
|
* import { createAnthropicAdapter } from '@ggui-ai/ui-gen/providers';
|
|
221
|
-
* import { createUiGenerator } from '@ggui-ai/ui-gen';
|
|
222
173
|
*
|
|
223
174
|
* const adapter = createAnthropicAdapter();
|
|
224
|
-
* const
|
|
225
|
-
*
|
|
175
|
+
* const result = await adapter.complete({
|
|
176
|
+
* apiKey,
|
|
177
|
+
* route: { provider: 'anthropic', model: 'claude-haiku-4-5-20251001' },
|
|
178
|
+
* systemPrompt,
|
|
179
|
+
* userPrompt,
|
|
180
|
+
* });
|
|
226
181
|
* ```
|
|
227
182
|
*
|
|
228
183
|
* Bedrock pool path (no API key — IAM at process boot):
|
package/dist/providers/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AnthropicBedrock } from '@anthropic-ai/bedrock-sdk';
|
|
1
|
+
import { AnthropicBedrockMantle, AnthropicBedrock } from '@anthropic-ai/bedrock-sdk';
|
|
2
2
|
|
|
3
3
|
// src/provider-adapter.ts
|
|
4
4
|
function makeProviderError(args) {
|
|
@@ -609,14 +609,19 @@ function createOpenRouterAdapter(options = {}) {
|
|
|
609
609
|
}
|
|
610
610
|
var PROVIDER5 = "bedrock";
|
|
611
611
|
var DEFAULT_MAX_TOKENS2 = 4096;
|
|
612
|
+
function bedrockEndpointFor(model) {
|
|
613
|
+
return model.startsWith("anthropic.") ? "mantle" : "runtime";
|
|
614
|
+
}
|
|
612
615
|
function createBedrockAdapter(options = {}) {
|
|
613
616
|
const region = options.region ?? process.env["AWS_REGION"] ?? "us-east-1";
|
|
614
|
-
const clientFactory = options.clientFactory ?? ((r) => new AnthropicBedrock({ awsRegion: r }));
|
|
615
|
-
|
|
616
|
-
function getClient() {
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
617
|
+
const clientFactory = options.clientFactory ?? ((r, endpoint) => endpoint === "mantle" ? new AnthropicBedrockMantle({ awsRegion: r }) : new AnthropicBedrock({ awsRegion: r }));
|
|
618
|
+
const cachedClients = {};
|
|
619
|
+
function getClient(endpoint) {
|
|
620
|
+
const cached = cachedClients[endpoint];
|
|
621
|
+
if (cached) return cached;
|
|
622
|
+
const created = clientFactory(region, endpoint);
|
|
623
|
+
cachedClients[endpoint] = created;
|
|
624
|
+
return created;
|
|
620
625
|
}
|
|
621
626
|
function mapError(raw) {
|
|
622
627
|
if (raw && typeof raw === "object" && "status" in raw) {
|
|
@@ -667,7 +672,7 @@ function createBedrockAdapter(options = {}) {
|
|
|
667
672
|
error: classifyFetchError(null, PROVIDER5, request.signal)
|
|
668
673
|
};
|
|
669
674
|
}
|
|
670
|
-
const client = getClient();
|
|
675
|
+
const client = getClient(bedrockEndpointFor(request.route.model));
|
|
671
676
|
let raw;
|
|
672
677
|
try {
|
|
673
678
|
raw = await client.messages.create(
|