@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
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import type Anthropic from '@anthropic-ai/sdk';
|
|
12
12
|
import type {
|
|
13
|
+
ImageBlockParam,
|
|
13
14
|
TextBlock,
|
|
14
15
|
TextBlockParam,
|
|
15
16
|
ToolUseBlock,
|
|
@@ -53,6 +54,51 @@ export interface AgentConfig {
|
|
|
53
54
|
* behavior for all existing callers).
|
|
54
55
|
*/
|
|
55
56
|
temperature?: number;
|
|
57
|
+
/**
|
|
58
|
+
* Explicit per-call credentials/routing, bypassing `process.env`
|
|
59
|
+
* entirely when supplied. Absent (default) preserves today's
|
|
60
|
+
* behavior — each agent falls back to reading its provider's env
|
|
61
|
+
* var(s) directly, so existing callers (including self-hosters who
|
|
62
|
+
* set env vars once at process start) are unaffected. Use this to
|
|
63
|
+
* run concurrent generations safely in one process: two simultaneous
|
|
64
|
+
* calls with different `routeOverride`s never race on shared
|
|
65
|
+
* `process.env` state.
|
|
66
|
+
*/
|
|
67
|
+
routeOverride?: {
|
|
68
|
+
readonly apiKey?: string;
|
|
69
|
+
/** Anthropic-only: force the Bedrock-IAM client instead of the direct API client. */
|
|
70
|
+
readonly useBedrock?: boolean;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Observer invoked once per retried attempt when `apiCall()` (#489)
|
|
74
|
+
* retries a rate-limited (HTTP 429) provider call. Fires AFTER the
|
|
75
|
+
* retry decision is made and BEFORE the delay is awaited — never on
|
|
76
|
+
* the terminal failure/success of the whole call. Purely an
|
|
77
|
+
* observability hook: it never influences whether or how long
|
|
78
|
+
* `apiCall()` retries. If the observer itself throws, `apiCall()`
|
|
79
|
+
* catches it, logs a `console.warn`, and proceeds to the retry
|
|
80
|
+
* delay unaffected — a broken observer cannot turn a recoverable
|
|
81
|
+
* 429 into an unrelated hard failure. Absent (default) is a no-op;
|
|
82
|
+
* `apiCall()` still logs every retry via `console.warn` regardless.
|
|
83
|
+
*/
|
|
84
|
+
onRetry?: (info: ProviderRetryInfo) => void;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** One retried attempt's context — passed to `AgentConfig.onRetry`. */
|
|
88
|
+
export interface ProviderRetryInfo {
|
|
89
|
+
readonly provider: AgentConfig['provider'];
|
|
90
|
+
/** 1-based index of this retry (1 = first retry, 2 = second, …). */
|
|
91
|
+
readonly attempt: number;
|
|
92
|
+
/** Total retries `apiCall()` will attempt before giving up. */
|
|
93
|
+
readonly maxAttempts: number;
|
|
94
|
+
/** HTTP status of the failed call, when the caught error carries one. */
|
|
95
|
+
readonly status: number | undefined;
|
|
96
|
+
/** Provider-supplied `Retry-After` value in seconds, when present and within the cap. */
|
|
97
|
+
readonly retryAfterSec: number | undefined;
|
|
98
|
+
/** Delay actually awaited before the next attempt. */
|
|
99
|
+
readonly delayMs: number;
|
|
100
|
+
/** One-line error summary (same shape as the existing `console.error` line). */
|
|
101
|
+
readonly message: string;
|
|
56
102
|
}
|
|
57
103
|
|
|
58
104
|
export interface LLMResponse {
|
|
@@ -61,6 +107,35 @@ export interface LLMResponse {
|
|
|
61
107
|
outputTokens: number;
|
|
62
108
|
}
|
|
63
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Image payload for {@link VisionAgent.callVision}. Base64-encoded
|
|
112
|
+
* bytes plus their media type — the two fields every vision-capable
|
|
113
|
+
* provider SDK needs to reconstruct the image part.
|
|
114
|
+
*/
|
|
115
|
+
export interface VisionImageInput {
|
|
116
|
+
mediaType: 'image/png' | 'image/jpeg' | 'image/webp';
|
|
117
|
+
base64: string;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The multimodal (image + text) call surface. Implemented by the
|
|
122
|
+
* providers whose SDK path supports vision input (Anthropic, Google);
|
|
123
|
+
* construct one via {@link createVisionAgent} so the call runs inside
|
|
124
|
+
* the same `apiCall()` retry/observability choke point as every text
|
|
125
|
+
* call — provider 429s retry with `Retry-After` honoring, `onRetry`
|
|
126
|
+
* reaches the caller's structured log, and `routeOverride` governs
|
|
127
|
+
* credentials and model routing instead of `process.env`.
|
|
128
|
+
*/
|
|
129
|
+
export interface VisionAgent {
|
|
130
|
+
callVision(
|
|
131
|
+
model: string,
|
|
132
|
+
systemPrompt: string,
|
|
133
|
+
userPrompt: string,
|
|
134
|
+
image: VisionImageInput,
|
|
135
|
+
maxTokens?: number,
|
|
136
|
+
): Promise<LLMResponse>;
|
|
137
|
+
}
|
|
138
|
+
|
|
64
139
|
export interface LLMTool {
|
|
65
140
|
name: string;
|
|
66
141
|
description: string;
|
|
@@ -167,6 +242,59 @@ function errorSummary(e: unknown): string {
|
|
|
167
242
|
return `${e.constructor.name}${status}${code}: ${e.message.slice(0, 120)}${body}`;
|
|
168
243
|
}
|
|
169
244
|
|
|
245
|
+
/** True when the caught value looks like an HTTP 429 from any provider SDK. */
|
|
246
|
+
function isRateLimited(e: unknown): boolean {
|
|
247
|
+
if (!(e instanceof Error) || !('status' in e)) return false;
|
|
248
|
+
return (e as { status: unknown }).status === 429;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** HTTP status of the caught error, when the SDK attached one. Duck-typed — matches `errorSummary`'s existing precedent. */
|
|
252
|
+
function extractStatus(e: unknown): number | undefined {
|
|
253
|
+
if (!(e instanceof Error) || !('status' in e)) return undefined;
|
|
254
|
+
const status = (e as { status: unknown }).status;
|
|
255
|
+
return typeof status === 'number' ? status : undefined;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Duck-typed `Retry-After` extraction. Both `@anthropic-ai/sdk`'s and
|
|
260
|
+
* `openai`'s `APIError` attach a `Headers`-like `.headers` (with
|
|
261
|
+
* `.get()`) to the thrown error at construction time — this is the
|
|
262
|
+
* ONLY point in the call chain where that header is still reachable
|
|
263
|
+
* (confirmed by reading both SDKs' error constructors). Handles the
|
|
264
|
+
* delta-seconds form only (a plain integer/float string) — the rarer
|
|
265
|
+
* HTTP-date form parses to `NaN` and correctly falls through to
|
|
266
|
+
* `undefined` rather than fabricating a value. Google's `ApiError` has
|
|
267
|
+
* no `.headers` at all, so this always returns `undefined` for Google
|
|
268
|
+
* — expected, not a gap.
|
|
269
|
+
*/
|
|
270
|
+
function extractRetryAfterSec(e: unknown): number | undefined {
|
|
271
|
+
if (!(e instanceof Error) || !('headers' in e)) return undefined;
|
|
272
|
+
const headers = (e as { headers?: unknown }).headers;
|
|
273
|
+
if (
|
|
274
|
+
headers === null ||
|
|
275
|
+
typeof headers !== 'object' ||
|
|
276
|
+
!('get' in headers) ||
|
|
277
|
+
typeof (headers as { get?: unknown }).get !== 'function'
|
|
278
|
+
) {
|
|
279
|
+
return undefined;
|
|
280
|
+
}
|
|
281
|
+
const raw = (headers as { get(name: string): string | null }).get('retry-after');
|
|
282
|
+
if (raw === null) return undefined;
|
|
283
|
+
const parsed = Number(raw);
|
|
284
|
+
return Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : undefined;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function sleep(ms: number): Promise<void> {
|
|
288
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Retry budget for `apiCall()` — see the docstring on `apiCall()` itself for the full policy. */
|
|
292
|
+
const MAX_RETRY_ATTEMPTS = 2;
|
|
293
|
+
const RETRY_AFTER_CAP_SEC = 15;
|
|
294
|
+
const DEFAULT_BACKOFF_BASE_MS = 1000;
|
|
295
|
+
const DEFAULT_BACKOFF_MAX_MS = 8000;
|
|
296
|
+
const MAX_TOTAL_RETRY_DELAY_MS = 20000;
|
|
297
|
+
|
|
170
298
|
// =============================================================================
|
|
171
299
|
// LLMAgent — abstract base
|
|
172
300
|
// =============================================================================
|
|
@@ -181,6 +309,21 @@ export abstract class LLMAgent {
|
|
|
181
309
|
// Call resetSession() between independent generation runs.
|
|
182
310
|
protected lastSessionId: string | undefined;
|
|
183
311
|
|
|
312
|
+
/**
|
|
313
|
+
* Explicit per-call routing override — see `AgentConfig.routeOverride`.
|
|
314
|
+
* Absent when constructed via the bare `new XAgent()` form (every
|
|
315
|
+
* existing call site), so `createClient()`/`resolveModel()` fall
|
|
316
|
+
* back to `process.env` exactly as before.
|
|
317
|
+
*/
|
|
318
|
+
protected readonly routeOverride: AgentConfig['routeOverride'];
|
|
319
|
+
|
|
320
|
+
/** See `AgentConfig.onRetry` (#489). Absent is a no-op. */
|
|
321
|
+
protected readonly onRetry: AgentConfig['onRetry'];
|
|
322
|
+
|
|
323
|
+
constructor(routeOverride?: AgentConfig['routeOverride'], onRetry?: AgentConfig['onRetry']) {
|
|
324
|
+
this.routeOverride = routeOverride;
|
|
325
|
+
this.onRetry = onRetry;
|
|
326
|
+
}
|
|
184
327
|
|
|
185
328
|
protected abstract resolveModel(model: string): string;
|
|
186
329
|
protected abstract createClient(): Promise<unknown>;
|
|
@@ -268,17 +411,92 @@ export abstract class LLMAgent {
|
|
|
268
411
|
}
|
|
269
412
|
|
|
270
413
|
/**
|
|
271
|
-
* Execute an API call
|
|
272
|
-
*
|
|
414
|
+
* Execute an API call, retrying once or twice on HTTP 429
|
|
415
|
+
* (rate-limited) before giving up (#489).
|
|
416
|
+
*
|
|
417
|
+
* Policy: on a 429, honor the provider's `Retry-After` header when
|
|
418
|
+
* present (capped at `RETRY_AFTER_CAP_SEC` — a provider asking for a
|
|
419
|
+
* longer wait is treated as "don't retry", not "wait longer": making
|
|
420
|
+
* the caller sit through 15+ more seconds on a single failed call is
|
|
421
|
+
* a bad experience regardless of what's driving the request);
|
|
422
|
+
* otherwise fall back to exponential backoff with jitter, capped at
|
|
423
|
+
* `DEFAULT_BACKOFF_MAX_MS` per attempt. Stops after
|
|
424
|
+
* `MAX_RETRY_ATTEMPTS` retries OR once the cumulative delay would
|
|
425
|
+
* exceed `MAX_TOTAL_RETRY_DELAY_MS`, whichever comes first. Any
|
|
426
|
+
* non-429 error, or a 429 with no attempts left, is logged and
|
|
427
|
+
* re-thrown immediately — unchanged from the pre-#489 behavior.
|
|
428
|
+
*
|
|
429
|
+
* **Budget scope (final-review correction):** `MAX_TOTAL_RETRY_DELAY_MS`
|
|
430
|
+
* bounds a single `apiCall()` invocation, not a whole generation. A
|
|
431
|
+
* generation issues many sequential `apiCall()` calls (coding turns,
|
|
432
|
+
* tool-result round-trips, eval rounds); under sustained throttling
|
|
433
|
+
* each one can independently add up to `MAX_TOTAL_RETRY_DELAY_MS`, so
|
|
434
|
+
* a generation with k rate-limited calls can add up to roughly `k *
|
|
435
|
+
* MAX_TOTAL_RETRY_DELAY_MS` to how long it holds its caller's
|
|
436
|
+
* resources (e.g. the caller's concurrency slot) — not a flat 20s
|
|
437
|
+
* ceiling. No shared per-generation budget is threaded here by
|
|
438
|
+
* design (accepted ruling, not an oversight): the caller's own
|
|
439
|
+
* queue/admission backstop is what bounds how long other
|
|
440
|
+
* queued work waits, independent of any one generation's retry
|
|
441
|
+
* total.
|
|
273
442
|
*/
|
|
274
443
|
protected async apiCall<T>(fn: () => Promise<T>): Promise<T> {
|
|
275
444
|
const start = Date.now();
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
445
|
+
let totalDelayMs = 0;
|
|
446
|
+
for (let attempt = 0; ; attempt++) {
|
|
447
|
+
try {
|
|
448
|
+
return await fn();
|
|
449
|
+
} catch (e: unknown) {
|
|
450
|
+
const ms = Date.now() - start;
|
|
451
|
+
if (attempt >= MAX_RETRY_ATTEMPTS || !isRateLimited(e)) {
|
|
452
|
+
console.error(`[${this.provider}] API error after ${ms}ms: ${errorSummary(e)}`);
|
|
453
|
+
throw e;
|
|
454
|
+
}
|
|
455
|
+
const retryAfterSec = extractRetryAfterSec(e);
|
|
456
|
+
if (retryAfterSec !== undefined && retryAfterSec > RETRY_AFTER_CAP_SEC) {
|
|
457
|
+
console.error(
|
|
458
|
+
`[${this.provider}] API error after ${ms}ms (retry-after ${retryAfterSec}s exceeds ${RETRY_AFTER_CAP_SEC}s cap, not retrying): ${errorSummary(e)}`,
|
|
459
|
+
);
|
|
460
|
+
throw e;
|
|
461
|
+
}
|
|
462
|
+
const delayMs =
|
|
463
|
+
retryAfterSec !== undefined
|
|
464
|
+
? retryAfterSec * 1000
|
|
465
|
+
: Math.min(DEFAULT_BACKOFF_BASE_MS * 2 ** attempt, DEFAULT_BACKOFF_MAX_MS) +
|
|
466
|
+
Math.floor(Math.random() * 250);
|
|
467
|
+
if (totalDelayMs + delayMs > MAX_TOTAL_RETRY_DELAY_MS) {
|
|
468
|
+
console.error(
|
|
469
|
+
`[${this.provider}] API error after ${ms}ms (retry budget exhausted, not retrying): ${errorSummary(e)}`,
|
|
470
|
+
);
|
|
471
|
+
throw e;
|
|
472
|
+
}
|
|
473
|
+
totalDelayMs += delayMs;
|
|
474
|
+
const info: ProviderRetryInfo = {
|
|
475
|
+
provider: this.provider,
|
|
476
|
+
attempt: attempt + 1,
|
|
477
|
+
maxAttempts: MAX_RETRY_ATTEMPTS,
|
|
478
|
+
status: extractStatus(e),
|
|
479
|
+
retryAfterSec,
|
|
480
|
+
delayMs,
|
|
481
|
+
message: errorSummary(e),
|
|
482
|
+
};
|
|
483
|
+
console.warn(
|
|
484
|
+
`[${this.provider}] rate-limited (attempt ${info.attempt}/${info.maxAttempts}), retrying in ${delayMs}ms`,
|
|
485
|
+
);
|
|
486
|
+
try {
|
|
487
|
+
this.onRetry?.(info);
|
|
488
|
+
} catch (observerError) {
|
|
489
|
+
// Guard, not propagate — the docstring on AgentConfig.onRetry
|
|
490
|
+
// promises this is "purely an observability hook: it never
|
|
491
|
+
// influences whether or how long apiCall() retries". A
|
|
492
|
+
// caller-supplied observer that throws must not turn a
|
|
493
|
+
// recoverable 429 into an unrelated hard failure.
|
|
494
|
+
console.warn(
|
|
495
|
+
`[${this.provider}] onRetry observer threw (ignored, retry proceeds): ${errorSummary(observerError)}`,
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
await sleep(delayMs);
|
|
499
|
+
}
|
|
282
500
|
}
|
|
283
501
|
}
|
|
284
502
|
|
|
@@ -294,7 +512,9 @@ export class AnthropicAgent extends LLMAgent {
|
|
|
294
512
|
protected resolveModel(model: string): string {
|
|
295
513
|
// Bedrock IAM path — the upstream model id must be the cross-region
|
|
296
514
|
// inference-profile form (`us.anthropic.*`), not the bare API id.
|
|
297
|
-
|
|
515
|
+
const useBedrock =
|
|
516
|
+
this.routeOverride?.useBedrock ?? process.env.CLAUDE_CODE_USE_BEDROCK === '1';
|
|
517
|
+
if (useBedrock) {
|
|
298
518
|
return getBedrockModelId(model);
|
|
299
519
|
}
|
|
300
520
|
return model.startsWith('anthropic/')
|
|
@@ -303,12 +523,14 @@ export class AnthropicAgent extends LLMAgent {
|
|
|
303
523
|
}
|
|
304
524
|
|
|
305
525
|
protected async createClient(): Promise<Anthropic | AnthropicBedrock> {
|
|
306
|
-
// Bedrock IAM path:
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
// `AnthropicBedrock` is wire-compatible with `Anthropic` for
|
|
310
|
-
// `.messages` API this agent uses.
|
|
311
|
-
|
|
526
|
+
// Bedrock IAM path: when CLAUDE_CODE_USE_BEDROCK=1 (set by the
|
|
527
|
+
// deployment's provider routing), auth comes from the ambient AWS
|
|
528
|
+
// credential chain (instance role, env credentials, etc.) — no API
|
|
529
|
+
// key. `AnthropicBedrock` is wire-compatible with `Anthropic` for
|
|
530
|
+
// the `.messages` API this agent uses.
|
|
531
|
+
const useBedrock =
|
|
532
|
+
this.routeOverride?.useBedrock ?? process.env.CLAUDE_CODE_USE_BEDROCK === '1';
|
|
533
|
+
if (useBedrock) {
|
|
312
534
|
const { AnthropicBedrock } = await import('@anthropic-ai/bedrock-sdk');
|
|
313
535
|
return new AnthropicBedrock({
|
|
314
536
|
awsRegion:
|
|
@@ -319,8 +541,10 @@ export class AnthropicAgent extends LLMAgent {
|
|
|
319
541
|
}
|
|
320
542
|
// SDK construction lives in `adapters/claude/client.ts`. The BYOK
|
|
321
543
|
// resolver writes the raw key into `process.env.ANTHROPIC_API_KEY`
|
|
322
|
-
// before this runs (see `applyRouteToEnv` in provider-router)
|
|
323
|
-
|
|
544
|
+
// before this runs (see `applyRouteToEnv` in provider-router) —
|
|
545
|
+
// `routeOverride.apiKey`, when supplied, skips that env round-trip
|
|
546
|
+
// entirely.
|
|
547
|
+
return createAnthropicClient(this.routeOverride?.apiKey ?? process.env.ANTHROPIC_API_KEY);
|
|
324
548
|
}
|
|
325
549
|
|
|
326
550
|
async callText(
|
|
@@ -413,6 +637,90 @@ export class AnthropicAgent extends LLMAgent {
|
|
|
413
637
|
}
|
|
414
638
|
}
|
|
415
639
|
|
|
640
|
+
async callVision(
|
|
641
|
+
model: string,
|
|
642
|
+
systemPrompt: string,
|
|
643
|
+
userPrompt: string,
|
|
644
|
+
image: VisionImageInput,
|
|
645
|
+
maxTokens?: number,
|
|
646
|
+
): Promise<LLMResponse> {
|
|
647
|
+
const client = await this.getClient<Anthropic>();
|
|
648
|
+
const traceId = newLlmTraceId();
|
|
649
|
+
const startedAt = Date.now();
|
|
650
|
+
const resolvedModel = this.resolveModel(model);
|
|
651
|
+
|
|
652
|
+
const imageBlock: ImageBlockParam = {
|
|
653
|
+
type: 'image',
|
|
654
|
+
source: {
|
|
655
|
+
type: 'base64',
|
|
656
|
+
media_type: image.mediaType,
|
|
657
|
+
data: image.base64,
|
|
658
|
+
},
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
try {
|
|
662
|
+
const response = await this.apiCall(() =>
|
|
663
|
+
client.messages.create({
|
|
664
|
+
model: resolvedModel,
|
|
665
|
+
max_tokens: maxTokens ?? 1500,
|
|
666
|
+
system: systemPrompt,
|
|
667
|
+
messages: [
|
|
668
|
+
{
|
|
669
|
+
role: 'user',
|
|
670
|
+
content: [imageBlock, { type: 'text', text: userPrompt }],
|
|
671
|
+
},
|
|
672
|
+
],
|
|
673
|
+
}),
|
|
674
|
+
);
|
|
675
|
+
|
|
676
|
+
const text = response.content
|
|
677
|
+
.filter((block): block is TextBlock => block.type === 'text')
|
|
678
|
+
.map((block) => block.text)
|
|
679
|
+
.join('');
|
|
680
|
+
|
|
681
|
+
const endedAt = Date.now();
|
|
682
|
+
// The image bytes stay OUT of the trace — a base64 screenshot
|
|
683
|
+
// would balloon every event; the prompts identify the call.
|
|
684
|
+
emitLlmTraceEvent({
|
|
685
|
+
id: traceId,
|
|
686
|
+
at: startedAt,
|
|
687
|
+
endedAt,
|
|
688
|
+
durationMs: endedAt - startedAt,
|
|
689
|
+
provider: 'anthropic',
|
|
690
|
+
model: resolvedModel,
|
|
691
|
+
kind: 'callVision',
|
|
692
|
+
systemPrompt,
|
|
693
|
+
userPrompt,
|
|
694
|
+
result: {
|
|
695
|
+
inputTokens: response.usage.input_tokens,
|
|
696
|
+
outputTokens: response.usage.output_tokens,
|
|
697
|
+
text,
|
|
698
|
+
},
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
return {
|
|
702
|
+
text,
|
|
703
|
+
inputTokens: response.usage.input_tokens,
|
|
704
|
+
outputTokens: response.usage.output_tokens,
|
|
705
|
+
};
|
|
706
|
+
} catch (e) {
|
|
707
|
+
const endedAt = Date.now();
|
|
708
|
+
emitLlmTraceEvent({
|
|
709
|
+
id: traceId,
|
|
710
|
+
at: startedAt,
|
|
711
|
+
endedAt,
|
|
712
|
+
durationMs: endedAt - startedAt,
|
|
713
|
+
provider: 'anthropic',
|
|
714
|
+
model: resolvedModel,
|
|
715
|
+
kind: 'callVision',
|
|
716
|
+
systemPrompt,
|
|
717
|
+
userPrompt,
|
|
718
|
+
error: { message: e instanceof Error ? e.message : String(e) },
|
|
719
|
+
});
|
|
720
|
+
throw e;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
416
724
|
async callTools(
|
|
417
725
|
model: string,
|
|
418
726
|
systemPrompt: string,
|
|
@@ -670,7 +978,7 @@ export class OpenAIAgent extends LLMAgent {
|
|
|
670
978
|
|
|
671
979
|
protected async createClient(): Promise<OpenAI> {
|
|
672
980
|
const { default: OpenAISDK } = await import('openai');
|
|
673
|
-
return new OpenAISDK({ apiKey: process.env.OPENAI_API_KEY });
|
|
981
|
+
return new OpenAISDK({ apiKey: this.routeOverride?.apiKey ?? process.env.OPENAI_API_KEY });
|
|
674
982
|
}
|
|
675
983
|
|
|
676
984
|
async callText(
|
|
@@ -928,7 +1236,8 @@ export class GoogleAgent extends LLMAgent {
|
|
|
928
1236
|
protected async createClient(): Promise<GoogleGenAI> {
|
|
929
1237
|
const { GoogleGenAI: GoogleGenAISDK } = await import('@google/genai');
|
|
930
1238
|
return new GoogleGenAISDK({
|
|
931
|
-
apiKey:
|
|
1239
|
+
apiKey:
|
|
1240
|
+
this.routeOverride?.apiKey ?? (process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY),
|
|
932
1241
|
httpOptions: { timeout: 300_000 }, // 5 min — Pro models can be slow
|
|
933
1242
|
});
|
|
934
1243
|
}
|
|
@@ -964,6 +1273,86 @@ export class GoogleAgent extends LLMAgent {
|
|
|
964
1273
|
};
|
|
965
1274
|
}
|
|
966
1275
|
|
|
1276
|
+
async callVision(
|
|
1277
|
+
model: string,
|
|
1278
|
+
systemPrompt: string,
|
|
1279
|
+
userPrompt: string,
|
|
1280
|
+
image: VisionImageInput,
|
|
1281
|
+
maxTokens?: number,
|
|
1282
|
+
): Promise<LLMResponse> {
|
|
1283
|
+
const client = await this.getClient<GoogleGenAI>();
|
|
1284
|
+
const traceId = newLlmTraceId();
|
|
1285
|
+
const startedAt = Date.now();
|
|
1286
|
+
const resolvedModel = this.resolveModel(model);
|
|
1287
|
+
|
|
1288
|
+
try {
|
|
1289
|
+
const response = await this.apiCall(() =>
|
|
1290
|
+
client.models.generateContent({
|
|
1291
|
+
model: resolvedModel,
|
|
1292
|
+
contents: [
|
|
1293
|
+
{
|
|
1294
|
+
role: 'user',
|
|
1295
|
+
parts: [
|
|
1296
|
+
{ inlineData: { mimeType: image.mediaType, data: image.base64 } },
|
|
1297
|
+
{ text: userPrompt },
|
|
1298
|
+
],
|
|
1299
|
+
},
|
|
1300
|
+
],
|
|
1301
|
+
config: {
|
|
1302
|
+
systemInstruction: systemPrompt,
|
|
1303
|
+
...(maxTokens ? { maxOutputTokens: maxTokens } : {}),
|
|
1304
|
+
},
|
|
1305
|
+
}),
|
|
1306
|
+
);
|
|
1307
|
+
|
|
1308
|
+
const text =
|
|
1309
|
+
response.candidates?.[0]?.content?.parts?.map((p) => p.text || '').join('') || '';
|
|
1310
|
+
const usage = response.usageMetadata;
|
|
1311
|
+
const result: LLMResponse = {
|
|
1312
|
+
text,
|
|
1313
|
+
inputTokens: usage?.promptTokenCount ?? 0,
|
|
1314
|
+
outputTokens: usage?.candidatesTokenCount ?? 0,
|
|
1315
|
+
};
|
|
1316
|
+
|
|
1317
|
+
const endedAt = Date.now();
|
|
1318
|
+
// The image bytes stay OUT of the trace — a base64 screenshot
|
|
1319
|
+
// would balloon every event; the prompts identify the call.
|
|
1320
|
+
emitLlmTraceEvent({
|
|
1321
|
+
id: traceId,
|
|
1322
|
+
at: startedAt,
|
|
1323
|
+
endedAt,
|
|
1324
|
+
durationMs: endedAt - startedAt,
|
|
1325
|
+
provider: 'google',
|
|
1326
|
+
model: resolvedModel,
|
|
1327
|
+
kind: 'callVision',
|
|
1328
|
+
systemPrompt,
|
|
1329
|
+
userPrompt,
|
|
1330
|
+
result: {
|
|
1331
|
+
inputTokens: result.inputTokens,
|
|
1332
|
+
outputTokens: result.outputTokens,
|
|
1333
|
+
text,
|
|
1334
|
+
},
|
|
1335
|
+
});
|
|
1336
|
+
|
|
1337
|
+
return result;
|
|
1338
|
+
} catch (e) {
|
|
1339
|
+
const endedAt = Date.now();
|
|
1340
|
+
emitLlmTraceEvent({
|
|
1341
|
+
id: traceId,
|
|
1342
|
+
at: startedAt,
|
|
1343
|
+
endedAt,
|
|
1344
|
+
durationMs: endedAt - startedAt,
|
|
1345
|
+
provider: 'google',
|
|
1346
|
+
model: resolvedModel,
|
|
1347
|
+
kind: 'callVision',
|
|
1348
|
+
systemPrompt,
|
|
1349
|
+
userPrompt,
|
|
1350
|
+
error: { message: e instanceof Error ? e.message : String(e) },
|
|
1351
|
+
});
|
|
1352
|
+
throw e;
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
|
|
967
1356
|
async callTools(
|
|
968
1357
|
model: string,
|
|
969
1358
|
systemPrompt: string,
|
|
@@ -1248,7 +1637,7 @@ export class OpenRouterAgent extends LLMAgent {
|
|
|
1248
1637
|
protected async createClient(): Promise<unknown> {
|
|
1249
1638
|
const { OpenRouterClient } = await import('../adapters/openrouter/client');
|
|
1250
1639
|
return new OpenRouterClient({
|
|
1251
|
-
apiKey: process.env.OPENROUTER_API_KEY!,
|
|
1640
|
+
apiKey: this.routeOverride?.apiKey ?? process.env.OPENROUTER_API_KEY!,
|
|
1252
1641
|
});
|
|
1253
1642
|
}
|
|
1254
1643
|
|
|
@@ -1432,17 +1821,52 @@ export class OpenRouterAgent extends LLMAgent {
|
|
|
1432
1821
|
/**
|
|
1433
1822
|
* Create a fresh agent instance for the given provider.
|
|
1434
1823
|
* Each call returns a new instance — no shared state between callers.
|
|
1824
|
+
*
|
|
1825
|
+
* Accepts either a bare provider string (every pre-existing call
|
|
1826
|
+
* site — the agent falls back to reading its provider's env var(s)
|
|
1827
|
+
* directly) or a full {@link AgentConfig}, whose `routeOverride`
|
|
1828
|
+
* (when present) is threaded into the constructed agent so it never
|
|
1829
|
+
* touches `process.env`.
|
|
1435
1830
|
*/
|
|
1436
|
-
export function createAgent(provider: AgentConfig['provider']): LLMAgent
|
|
1831
|
+
export function createAgent(provider: AgentConfig['provider']): LLMAgent;
|
|
1832
|
+
export function createAgent(config: AgentConfig): LLMAgent;
|
|
1833
|
+
export function createAgent(providerOrConfig: AgentConfig['provider'] | AgentConfig): LLMAgent {
|
|
1834
|
+
const provider =
|
|
1835
|
+
typeof providerOrConfig === 'string' ? providerOrConfig : providerOrConfig.provider;
|
|
1836
|
+
const routeOverride =
|
|
1837
|
+
typeof providerOrConfig === 'string' ? undefined : providerOrConfig.routeOverride;
|
|
1838
|
+
const onRetry = typeof providerOrConfig === 'string' ? undefined : providerOrConfig.onRetry;
|
|
1437
1839
|
switch (provider) {
|
|
1438
1840
|
case 'anthropic':
|
|
1439
|
-
return new AnthropicAgent();
|
|
1841
|
+
return new AnthropicAgent(routeOverride, onRetry);
|
|
1440
1842
|
case 'openai':
|
|
1441
|
-
return new OpenAIAgent();
|
|
1843
|
+
return new OpenAIAgent(routeOverride, onRetry);
|
|
1442
1844
|
case 'google':
|
|
1443
|
-
return new GoogleAgent();
|
|
1845
|
+
return new GoogleAgent(routeOverride, onRetry);
|
|
1444
1846
|
case 'openrouter':
|
|
1445
|
-
return new OpenRouterAgent();
|
|
1847
|
+
return new OpenRouterAgent(routeOverride, onRetry);
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
/** Providers whose agent implements {@link VisionAgent}. */
|
|
1852
|
+
export type VisionProvider = 'anthropic' | 'google';
|
|
1853
|
+
|
|
1854
|
+
/**
|
|
1855
|
+
* Construct an agent for a multimodal (image + text) call. Same
|
|
1856
|
+
* threading contract as {@link createAgent} — `routeOverride` and
|
|
1857
|
+
* `onRetry` from the config reach the constructed agent — with the
|
|
1858
|
+
* provider narrowed to the ones whose SDK path supports vision input,
|
|
1859
|
+
* so the returned type carries `callVision` statically instead of
|
|
1860
|
+
* every caller re-proving it.
|
|
1861
|
+
*/
|
|
1862
|
+
export function createVisionAgent(
|
|
1863
|
+
config: AgentConfig & { provider: VisionProvider },
|
|
1864
|
+
): LLMAgent & VisionAgent {
|
|
1865
|
+
switch (config.provider) {
|
|
1866
|
+
case 'anthropic':
|
|
1867
|
+
return new AnthropicAgent(config.routeOverride, config.onRetry);
|
|
1868
|
+
case 'google':
|
|
1869
|
+
return new GoogleAgent(config.routeOverride, config.onRetry);
|
|
1446
1870
|
}
|
|
1447
1871
|
}
|
|
1448
1872
|
|
|
@@ -1457,7 +1881,7 @@ export async function callLLM(
|
|
|
1457
1881
|
maxTokens?: number,
|
|
1458
1882
|
): Promise<LLMResponse> {
|
|
1459
1883
|
const start = Date.now();
|
|
1460
|
-
const agent = createAgent(config
|
|
1884
|
+
const agent = createAgent(config);
|
|
1461
1885
|
const result = await agent.callText(
|
|
1462
1886
|
config.model,
|
|
1463
1887
|
systemPrompt,
|
|
@@ -1479,7 +1903,7 @@ export async function callLLMWithTools(
|
|
|
1479
1903
|
maxTurns: number = 10,
|
|
1480
1904
|
): Promise<LLMWithToolsResponse> {
|
|
1481
1905
|
const start = Date.now();
|
|
1482
|
-
const agent = createAgent(config
|
|
1906
|
+
const agent = createAgent(config);
|
|
1483
1907
|
const result = await agent.callWithTools(
|
|
1484
1908
|
config.model,
|
|
1485
1909
|
systemPrompt,
|
package/src/harness/runtime.ts
CHANGED
|
@@ -31,6 +31,7 @@ import { WIRE_DOCUMENTATION } from "../tools/get-wire.js";
|
|
|
31
31
|
import { renderPitfallsBlock } from "./pitfalls.js";
|
|
32
32
|
import type { GadgetDescriptor, DataContract, JsonObject } from "@ggui-ai/protocol";
|
|
33
33
|
import { buildSystemPrompt as buildSystemPromptSkeleton } from "../boilerplate.js";
|
|
34
|
+
import type { AgentConfig } from "./llm-router.js";
|
|
34
35
|
|
|
35
36
|
// Re-export the boilerplate generator so existing internal importers
|
|
36
37
|
// (create-harness.ts, benchmarks/preview-boilerplate.ts) keep working.
|
|
@@ -46,6 +47,20 @@ type Provider = "anthropic" | "openai" | "google" | "openrouter";
|
|
|
46
47
|
export interface AgentSpec {
|
|
47
48
|
provider: Provider;
|
|
48
49
|
model: string;
|
|
50
|
+
/**
|
|
51
|
+
* Explicit per-call credentials/routing (#484) — see
|
|
52
|
+
* `AgentConfig.routeOverride`. Threaded through unchanged so
|
|
53
|
+
* `createAgent()` can bypass `process.env` when the caller supplies
|
|
54
|
+
* it. Absent for every pre-existing caller.
|
|
55
|
+
*/
|
|
56
|
+
routeOverride?: AgentConfig["routeOverride"];
|
|
57
|
+
/**
|
|
58
|
+
* Retry observer (#489) — see `AgentConfig.onRetry`. Threaded
|
|
59
|
+
* through unchanged so `createAgent()` can notify the caller when
|
|
60
|
+
* `apiCall()` retries a rate-limited request. Absent for every
|
|
61
|
+
* pre-existing caller (no-op).
|
|
62
|
+
*/
|
|
63
|
+
onRetry?: AgentConfig["onRetry"];
|
|
49
64
|
}
|
|
50
65
|
|
|
51
66
|
export interface SingleComponentParams {
|
package/src/index.ts
CHANGED
|
@@ -2,14 +2,13 @@
|
|
|
2
2
|
* `@ggui-ai/ui-gen` — open-source UI generation harness.
|
|
3
3
|
*
|
|
4
4
|
* Implements the `UiGenerator` contract from `@ggui-ai/mcp-server-core`.
|
|
5
|
-
* `createUiGenerator()` returns a callable provider-backed generator
|
|
5
|
+
* `createUiGenerator()` returns a callable provider-backed generator.
|
|
6
|
+
* Provider routing, compilation, and the system prompt are all
|
|
7
|
+
* internal to the harness — the factory takes no `adapter` option:
|
|
6
8
|
*
|
|
7
9
|
* import { createUiGenerator } from '@ggui-ai/ui-gen';
|
|
8
|
-
* import { createAnthropicAdapter } from '@ggui-ai/ui-gen/providers';
|
|
9
10
|
*
|
|
10
|
-
* const generator = createUiGenerator(
|
|
11
|
-
* adapter: createAnthropicAdapter(),
|
|
12
|
-
* });
|
|
11
|
+
* const generator = createUiGenerator();
|
|
13
12
|
* const result = await generator.generate({ request, llm, providerKey, blueprints });
|
|
14
13
|
*
|
|
15
14
|
* The `./harness` / `./workflows` / `./classifier` / `./fragments`
|
|
@@ -21,6 +20,7 @@ export {
|
|
|
21
20
|
extractComponentCode,
|
|
22
21
|
} from './create-ui-generator.js';
|
|
23
22
|
export type { CreateUiGeneratorOptions } from './create-ui-generator.js';
|
|
23
|
+
export type { ProviderRetryInfo } from './harness/llm-router.js';
|
|
24
24
|
|
|
25
25
|
// Generator registry seam re-export. The interface + slug helpers live
|
|
26
26
|
// in `@ggui-ai/mcp-server-core`; the in-memory factory lives in
|