@ggui-ai/ui-gen 0.9.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.
Files changed (87) hide show
  1. package/dist/adapters/generation-dispatch.d.ts +9 -2
  2. package/dist/adapters/generation-dispatch.js +547 -85
  3. package/dist/adapters/generation-dispatch.js.map +1 -1
  4. package/dist/adapters/index.js +28 -11
  5. package/dist/adapters/index.js.map +1 -1
  6. package/dist/advanced/index.d.ts +5 -3
  7. package/dist/advanced/index.js +563 -92
  8. package/dist/advanced/index.js.map +1 -1
  9. package/dist/blueprint-validator.js +109 -3
  10. package/dist/blueprint-validator.js.map +1 -1
  11. package/dist/boilerplate.js +14 -2
  12. package/dist/boilerplate.js.map +1 -1
  13. package/dist/check/index.d.ts +1 -1
  14. package/dist/check/index.js +11 -1
  15. package/dist/check/index.js.map +1 -1
  16. package/dist/{create-ui-generator-B9kV4U9h.d.ts → create-ui-generator-CF0EL0Li.d.ts} +13 -1
  17. package/dist/{dispatch-Deo9qzrG.d.ts → dispatch-D75M-OfZ.d.ts} +1 -1
  18. package/dist/evaluation/axis-checks/index.d.ts +3 -3
  19. package/dist/evaluation/axis-checks/index.js +101 -0
  20. package/dist/evaluation/axis-checks/index.js.map +1 -1
  21. package/dist/evaluation/axis-checks/registry.d.ts +1 -1
  22. package/dist/evaluation/axis-checks/registry.js +101 -0
  23. package/dist/evaluation/axis-checks/registry.js.map +1 -1
  24. package/dist/evaluation/index.d.ts +46 -3
  25. package/dist/evaluation/index.js +358 -17
  26. package/dist/evaluation/index.js.map +1 -1
  27. package/dist/harness/check/runtime-render/index.d.ts +2 -2
  28. package/dist/harness/check/runtime-render/index.js +110 -3
  29. package/dist/harness/check/runtime-render/index.js.map +1 -1
  30. package/dist/harness/index.d.ts +2 -2
  31. package/dist/harness/index.js +263 -18
  32. package/dist/harness/index.js.map +1 -1
  33. package/dist/harness/llm-trace-sink.d.ts +1 -1
  34. package/dist/harness/llm-trace-sink.js.map +1 -1
  35. package/dist/harness/prompts.js +17 -3
  36. package/dist/harness/prompts.js.map +1 -1
  37. package/dist/harness/result-types.d.ts +1 -1
  38. package/dist/harness/runtime.d.ts +9 -2
  39. package/dist/harness/runtime.js +20 -6
  40. package/dist/harness/runtime.js.map +1 -1
  41. package/dist/harness/types-public.d.ts +1 -1
  42. package/dist/harness/types-public.js +14 -2
  43. package/dist/harness/types-public.js.map +1 -1
  44. package/dist/harness/validator-trace-sink.d.ts +1 -1
  45. package/dist/index.d.ts +5 -3
  46. package/dist/index.js +563 -92
  47. package/dist/index.js.map +1 -1
  48. package/dist/{llm-router-1TyI0_TV.d.ts → llm-router-BEVtbAb9.d.ts} +61 -4
  49. package/dist/patch.d.ts +1 -1
  50. package/dist/provider-adapter.d.ts +16 -6
  51. package/dist/provider-adapter.js.map +1 -1
  52. package/dist/providers/index.d.ts +19 -6
  53. package/dist/providers/index.js.map +1 -1
  54. package/dist/{render-check-CNZjHD5g.d.ts → render-check-BNYGVDJd.d.ts} +1 -1
  55. package/dist/{types-public-C36k1q6s.d.ts → types-public-DIpjC-OA.d.ts} +1 -1
  56. package/dist/workflows.d.ts +1 -1
  57. package/package.json +12 -12
  58. package/src/adapters/generation-dispatch.ts +9 -0
  59. package/src/adapters/openrouter/client.ts +2 -2
  60. package/src/adapters/openrouter/types.ts +10 -0
  61. package/src/boilerplate/generate.ts +10 -2
  62. package/src/boilerplate/json-schema-ts.ts +22 -2
  63. package/src/boilerplate/system-prompt.ts +1 -0
  64. package/src/compile.ts +1 -1
  65. package/src/contract-context.ts +10 -2
  66. package/src/create-ui-generator.ts +14 -0
  67. package/src/design-system-docs.ts +6 -4
  68. package/src/evaluation/axis-checks/checks/state-ui-affordance.ts +162 -0
  69. package/src/evaluation/axis-checks/registry.ts +2 -0
  70. package/src/evaluation/index.ts +11 -0
  71. package/src/evaluation/llm-evaluator.ts +14 -1
  72. package/src/evaluation/visual-evaluator.ts +44 -65
  73. package/src/harness/check/runtime-render/adapter.ts +18 -0
  74. package/src/harness/check/runtime-render/index.ts +8 -0
  75. package/src/harness/check/runtime-render/prepare-mockup.ts +14 -0
  76. package/src/harness/check/runtime-render/production-act-shim.ts +158 -0
  77. package/src/harness/check/runtime-render/render-check.ts +72 -1
  78. package/src/harness/coding/run-eval-round.ts +61 -5
  79. package/src/harness/index.ts +1 -1
  80. package/src/harness/llm-router.ts +397 -18
  81. package/src/harness/llm-trace-sink.ts +2 -1
  82. package/src/harness/runtime.ts +7 -0
  83. package/src/index.ts +5 -5
  84. package/src/provider-adapter.ts +16 -6
  85. package/src/providers/bedrock.ts +5 -4
  86. package/src/providers/index.ts +16 -4
  87. package/src/tools/render-check.ts +19 -7
@@ -1,7 +1,7 @@
1
1
  import { Harness } from './types-public.js';
2
2
  export { CheckLeg, CheckResult, CreateHarnessInput, DefaultTaskRunner, HarnessConstructionContext, HarnessId, HarnessMeta, HarnessName, HarnessOverrides, HarnessRevision, HowLeg, IterationRecord, LLMEvaluator, PatchFn, Phase, PhaseRunResult, PlannerDecision, PlannerFn, ProcessLeg, PromptBuilder, RetryPolicy, RunCheckInput, RunHarnessInput, RunHarnessReason, RunHarnessResult, RunWorkflowInput, Task, TaskContext, TaskRunner, TierCheck, WhatLeg, Workflow, WorkflowRunResult, createHarness, runCheck, runHarness, runWorkflow } from './types-public.js';
3
3
  export { applyLineRanges, defaultApplyPatch } from '../patch.js';
4
- export { A as AgentConfig, L as LLMResponse, c as callLLM, a as createAgent } from '../llm-router-1TyI0_TV.js';
4
+ export { A as AgentConfig, L as LLMResponse, P as ProviderRetryInfo, c as callLLM, a as createAgent } from '../llm-router-BEVtbAb9.js';
5
5
  export { WORKFLOWS, WorkflowId, pickWorkflow } from '../workflows.js';
6
6
  export { LlmTraceEvent, LlmTraceKind, LlmTraceProvider, LlmTraceSink, getLlmTraceSink, setLlmTraceSink } from './llm-trace-sink.js';
7
7
  export { ValidatorTraceEvent, ValidatorTraceSink, getValidatorTraceSink, setValidatorTraceSink } from './validator-trace-sink.js';
@@ -9,7 +9,7 @@ export { ProcessMode } from '../policy.js';
9
9
  export { computeHarnessId, computeHarnessName, hashClassification } from '../hash.js';
10
10
  import '@ggui-ai/protocol';
11
11
  import '../axes-CzLEMDeB.js';
12
- import '../types-public-C36k1q6s.js';
12
+ import '../types-public-DIpjC-OA.js';
13
13
  import '../types-BOvHNG7K.js';
14
14
  import '../llm.js';
15
15
 
@@ -27,14 +27,16 @@ var OpenRouterError;
27
27
  var init_types = __esm({
28
28
  "src/adapters/openrouter/types.ts"() {
29
29
  OpenRouterError = class extends Error {
30
- constructor(message, status, code) {
30
+ constructor(message, status, code, headers) {
31
31
  super(message);
32
32
  this.status = status;
33
33
  this.code = code;
34
+ this.headers = headers;
34
35
  this.name = "OpenRouterError";
35
36
  }
36
37
  status;
37
38
  code;
39
+ headers;
38
40
  get isTransient() {
39
41
  return this.status === 408 || this.status === 429 || this.status === 502 || this.status === 503;
40
42
  }
@@ -79,7 +81,7 @@ var init_client = __esm({
79
81
  if (!response.ok) {
80
82
  const errorBody = await response.json().catch(() => ({ error: { message: response.statusText } }));
81
83
  const message = errorBody.error?.message ?? response.statusText;
82
- throw new OpenRouterError(message, response.status);
84
+ throw new OpenRouterError(message, response.status, void 0, response.headers);
83
85
  }
84
86
  return response.json();
85
87
  }
@@ -97,7 +99,7 @@ var init_client = __esm({
97
99
  if (!response.ok) {
98
100
  const errorBody = await response.json().catch(() => ({ error: { message: response.statusText } }));
99
101
  const message = errorBody.error?.message ?? response.statusText;
100
- throw new OpenRouterError(message, response.status);
102
+ throw new OpenRouterError(message, response.status, void 0, response.headers);
101
103
  }
102
104
  if (!response.body) {
103
105
  throw new OpenRouterError("No response body for streaming request", 500);
@@ -883,6 +885,15 @@ function jsonSchemaTypeToTs(schema) {
883
885
  if (schema.enum?.length) {
884
886
  return schema.enum.map((v) => typeof v === "string" ? `'${v}'` : String(v)).join(" | ");
885
887
  }
888
+ if (Array.isArray(schema.type)) {
889
+ const { nullable: _nullable, ...rest } = schema;
890
+ const members = [...new Set(schema.type)].map(
891
+ (member) => jsonSchemaTypeToTs({ ...rest, type: member })
892
+ );
893
+ const unique = [...new Set(members)];
894
+ if (schema.nullable && !unique.includes("null")) unique.push("null");
895
+ return unique.join(" | ");
896
+ }
886
897
  let result;
887
898
  switch (schema.type) {
888
899
  case "string":
@@ -900,7 +911,8 @@ function jsonSchemaTypeToTs(schema) {
900
911
  case "array": {
901
912
  if (schema.items) {
902
913
  const itemType = jsonSchemaTypeToTs(schema.items);
903
- result = schema.items.type === "object" && schema.items.properties ? `Array<${itemType}>` : itemType.includes("|") ? `(${itemType})[]` : `${itemType}[]`;
914
+ const itemsDeclareObject = Array.isArray(schema.items.type) ? schema.items.type.includes("object") : schema.items.type === "object";
915
+ result = itemsDeclareObject && schema.items.properties ? `Array<${itemType}>` : itemType.includes("|") ? `(${itemType})[]` : `${itemType}[]`;
904
916
  } else {
905
917
  result = "unknown[]";
906
918
  }
@@ -1033,13 +1045,14 @@ function generateBoilerplate(_userPrompt, contract, shellType, screen, composedS
1033
1045
  const propsFields = [];
1034
1046
  const propsProperties = contract?.propsSpec?.properties ?? {};
1035
1047
  for (const [key, entry] of Object.entries(propsProperties)) {
1036
- const required = entry.required !== false;
1048
+ const required = entry.required === true;
1037
1049
  const nullable = entry.schema.nullable === true;
1038
1050
  const tsType = jsonSchemaTypeToTs(entry.schema);
1039
1051
  const fullType = nullable ? `${tsType} | null` : tsType;
1040
1052
  const parts = [];
1041
1053
  if (entry.description) parts.push(entry.description);
1042
1054
  if (entry.default !== void 0) parts.push(`(default: ${JSON.stringify(entry.default)})`);
1055
+ if (!required) parts.push("(optional \u2014 the agent may omit it; guard or fall back)");
1043
1056
  const desc = parts.length > 0 ? ` // ${parts.join(" ")}` : "";
1044
1057
  propsFields.push(` ${key}${required ? "" : "?"}: ${fullType};${desc}`);
1045
1058
  }
@@ -1787,6 +1800,7 @@ A \`DataContract\` declares everything a GguiSession exchanges with the outside
1787
1800
 
1788
1801
  Props arrive at mount and may be partial on first render; later prop changes reach the SAME mounted component via \`ggui_amend\` (in-place repaint) on the live channel. Stream channels start empty and fill over time. Context slots start at their declared default (often \`null\`). **Never assume a field exists before you read it.**
1789
1802
 
1803
+ - **Optional contract props**: a \`Props\` field marked \`?\` in the boilerplate may be omitted by the agent ENTIRELY \u2014 the wire gate only enforces \`required: true\` entries, and the harness verifies your component with those props stripped. Guard them (\`?? default\`, \`?.\`, or an empty-state branch) from the FIRST render.
1790
1804
  - **Array iteration**: always default to \`[]\` before \`.map\`/\`.filter\`/\`.length\`. Use \`(props.items ?? []).map(...)\` not \`props.items.map(...)\`. Same for stream.all, stream.latest, etc.
1791
1805
  - **Object access**: optional-chain through nested fields. \`props.user?.name ?? 'Anonymous'\` not \`props.user.name\`.
1792
1806
  - **Number ops**: default before arithmetic. \`(props.count ?? 0) + 1\` not \`props.count + 1\`.
@@ -2816,6 +2830,34 @@ function errorSummary(e) {
2816
2830
  }
2817
2831
  return `${e.constructor.name}${status}${code}: ${e.message.slice(0, 120)}${body}`;
2818
2832
  }
2833
+ function isRateLimited(e) {
2834
+ if (!(e instanceof Error) || !("status" in e)) return false;
2835
+ return e.status === 429;
2836
+ }
2837
+ function extractStatus(e) {
2838
+ if (!(e instanceof Error) || !("status" in e)) return void 0;
2839
+ const status = e.status;
2840
+ return typeof status === "number" ? status : void 0;
2841
+ }
2842
+ function extractRetryAfterSec(e) {
2843
+ if (!(e instanceof Error) || !("headers" in e)) return void 0;
2844
+ const headers = e.headers;
2845
+ if (headers === null || typeof headers !== "object" || !("get" in headers) || typeof headers.get !== "function") {
2846
+ return void 0;
2847
+ }
2848
+ const raw = headers.get("retry-after");
2849
+ if (raw === null) return void 0;
2850
+ const parsed = Number(raw);
2851
+ return Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : void 0;
2852
+ }
2853
+ function sleep(ms) {
2854
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
2855
+ }
2856
+ var MAX_RETRY_ATTEMPTS = 2;
2857
+ var RETRY_AFTER_CAP_SEC = 15;
2858
+ var DEFAULT_BACKOFF_BASE_MS = 1e3;
2859
+ var DEFAULT_BACKOFF_MAX_MS = 8e3;
2860
+ var MAX_TOTAL_RETRY_DELAY_MS = 2e4;
2819
2861
  var LLMAgent = class {
2820
2862
  client = null;
2821
2863
  // ── Session state for server-side chaining ──────────────
@@ -2830,8 +2872,11 @@ var LLMAgent = class {
2830
2872
  * back to `process.env` exactly as before.
2831
2873
  */
2832
2874
  routeOverride;
2833
- constructor(routeOverride) {
2875
+ /** See `AgentConfig.onRetry` (#489). Absent is a no-op. */
2876
+ onRetry;
2877
+ constructor(routeOverride, onRetry) {
2834
2878
  this.routeOverride = routeOverride;
2879
+ this.onRetry = onRetry;
2835
2880
  }
2836
2881
  async getClient() {
2837
2882
  if (!this.client) {
@@ -2866,17 +2911,83 @@ var LLMAgent = class {
2866
2911
  async sendToolResult(_results) {
2867
2912
  }
2868
2913
  /**
2869
- * Execute an API call. No retry — if it fails, it fails.
2870
- * Logs the error with provider context and re-throws.
2914
+ * Execute an API call, retrying once or twice on HTTP 429
2915
+ * (rate-limited) before giving up (#489).
2916
+ *
2917
+ * Policy: on a 429, honor the provider's `Retry-After` header when
2918
+ * present (capped at `RETRY_AFTER_CAP_SEC` — a provider asking for a
2919
+ * longer wait is treated as "don't retry", not "wait longer": making
2920
+ * the caller sit through 15+ more seconds on a single failed call is
2921
+ * a bad experience regardless of what's driving the request);
2922
+ * otherwise fall back to exponential backoff with jitter, capped at
2923
+ * `DEFAULT_BACKOFF_MAX_MS` per attempt. Stops after
2924
+ * `MAX_RETRY_ATTEMPTS` retries OR once the cumulative delay would
2925
+ * exceed `MAX_TOTAL_RETRY_DELAY_MS`, whichever comes first. Any
2926
+ * non-429 error, or a 429 with no attempts left, is logged and
2927
+ * re-thrown immediately — unchanged from the pre-#489 behavior.
2928
+ *
2929
+ * **Budget scope (final-review correction):** `MAX_TOTAL_RETRY_DELAY_MS`
2930
+ * bounds a single `apiCall()` invocation, not a whole generation. A
2931
+ * generation issues many sequential `apiCall()` calls (coding turns,
2932
+ * tool-result round-trips, eval rounds); under sustained throttling
2933
+ * each one can independently add up to `MAX_TOTAL_RETRY_DELAY_MS`, so
2934
+ * a generation with k rate-limited calls can add up to roughly `k *
2935
+ * MAX_TOTAL_RETRY_DELAY_MS` to how long it holds its caller's
2936
+ * resources (e.g. the caller's concurrency slot) — not a flat 20s
2937
+ * ceiling. No shared per-generation budget is threaded here by
2938
+ * design (accepted ruling, not an oversight): the caller's own
2939
+ * queue/admission backstop is what bounds how long other
2940
+ * queued work waits, independent of any one generation's retry
2941
+ * total.
2871
2942
  */
2872
2943
  async apiCall(fn) {
2873
2944
  const start = Date.now();
2874
- try {
2875
- return await fn();
2876
- } catch (e) {
2877
- const ms = Date.now() - start;
2878
- console.error(`[${this.provider}] API error after ${ms}ms: ${errorSummary(e)}`);
2879
- throw e;
2945
+ let totalDelayMs = 0;
2946
+ for (let attempt = 0; ; attempt++) {
2947
+ try {
2948
+ return await fn();
2949
+ } catch (e) {
2950
+ const ms = Date.now() - start;
2951
+ if (attempt >= MAX_RETRY_ATTEMPTS || !isRateLimited(e)) {
2952
+ console.error(`[${this.provider}] API error after ${ms}ms: ${errorSummary(e)}`);
2953
+ throw e;
2954
+ }
2955
+ const retryAfterSec = extractRetryAfterSec(e);
2956
+ if (retryAfterSec !== void 0 && retryAfterSec > RETRY_AFTER_CAP_SEC) {
2957
+ console.error(
2958
+ `[${this.provider}] API error after ${ms}ms (retry-after ${retryAfterSec}s exceeds ${RETRY_AFTER_CAP_SEC}s cap, not retrying): ${errorSummary(e)}`
2959
+ );
2960
+ throw e;
2961
+ }
2962
+ const delayMs = retryAfterSec !== void 0 ? retryAfterSec * 1e3 : Math.min(DEFAULT_BACKOFF_BASE_MS * 2 ** attempt, DEFAULT_BACKOFF_MAX_MS) + Math.floor(Math.random() * 250);
2963
+ if (totalDelayMs + delayMs > MAX_TOTAL_RETRY_DELAY_MS) {
2964
+ console.error(
2965
+ `[${this.provider}] API error after ${ms}ms (retry budget exhausted, not retrying): ${errorSummary(e)}`
2966
+ );
2967
+ throw e;
2968
+ }
2969
+ totalDelayMs += delayMs;
2970
+ const info = {
2971
+ provider: this.provider,
2972
+ attempt: attempt + 1,
2973
+ maxAttempts: MAX_RETRY_ATTEMPTS,
2974
+ status: extractStatus(e),
2975
+ retryAfterSec,
2976
+ delayMs,
2977
+ message: errorSummary(e)
2978
+ };
2979
+ console.warn(
2980
+ `[${this.provider}] rate-limited (attempt ${info.attempt}/${info.maxAttempts}), retrying in ${delayMs}ms`
2981
+ );
2982
+ try {
2983
+ this.onRetry?.(info);
2984
+ } catch (observerError) {
2985
+ console.warn(
2986
+ `[${this.provider}] onRetry observer threw (ignored, retry proceeds): ${errorSummary(observerError)}`
2987
+ );
2988
+ }
2989
+ await sleep(delayMs);
2990
+ }
2880
2991
  }
2881
2992
  }
2882
2993
  };
@@ -2969,6 +3080,73 @@ var AnthropicAgent = class extends LLMAgent {
2969
3080
  throw e;
2970
3081
  }
2971
3082
  }
3083
+ async callVision(model, systemPrompt, userPrompt, image, maxTokens) {
3084
+ const client = await this.getClient();
3085
+ const traceId = newLlmTraceId();
3086
+ const startedAt = Date.now();
3087
+ const resolvedModel = this.resolveModel(model);
3088
+ const imageBlock = {
3089
+ type: "image",
3090
+ source: {
3091
+ type: "base64",
3092
+ media_type: image.mediaType,
3093
+ data: image.base64
3094
+ }
3095
+ };
3096
+ try {
3097
+ const response = await this.apiCall(
3098
+ () => client.messages.create({
3099
+ model: resolvedModel,
3100
+ max_tokens: maxTokens ?? 1500,
3101
+ system: systemPrompt,
3102
+ messages: [
3103
+ {
3104
+ role: "user",
3105
+ content: [imageBlock, { type: "text", text: userPrompt }]
3106
+ }
3107
+ ]
3108
+ })
3109
+ );
3110
+ const text = response.content.filter((block) => block.type === "text").map((block) => block.text).join("");
3111
+ const endedAt = Date.now();
3112
+ emitLlmTraceEvent({
3113
+ id: traceId,
3114
+ at: startedAt,
3115
+ endedAt,
3116
+ durationMs: endedAt - startedAt,
3117
+ provider: "anthropic",
3118
+ model: resolvedModel,
3119
+ kind: "callVision",
3120
+ systemPrompt,
3121
+ userPrompt,
3122
+ result: {
3123
+ inputTokens: response.usage.input_tokens,
3124
+ outputTokens: response.usage.output_tokens,
3125
+ text
3126
+ }
3127
+ });
3128
+ return {
3129
+ text,
3130
+ inputTokens: response.usage.input_tokens,
3131
+ outputTokens: response.usage.output_tokens
3132
+ };
3133
+ } catch (e) {
3134
+ const endedAt = Date.now();
3135
+ emitLlmTraceEvent({
3136
+ id: traceId,
3137
+ at: startedAt,
3138
+ endedAt,
3139
+ durationMs: endedAt - startedAt,
3140
+ provider: "anthropic",
3141
+ model: resolvedModel,
3142
+ kind: "callVision",
3143
+ systemPrompt,
3144
+ userPrompt,
3145
+ error: { message: e instanceof Error ? e.message : String(e) }
3146
+ });
3147
+ throw e;
3148
+ }
3149
+ }
2972
3150
  async callTools(model, systemPrompt, userPrompt, tools, toolChoice = "required", _scopedTools) {
2973
3151
  const client = await this.getClient();
2974
3152
  const traceId = newLlmTraceId();
@@ -3364,6 +3542,72 @@ var GoogleAgent = class extends LLMAgent {
3364
3542
  outputTokens: interaction.usage?.total_output_tokens ?? 0
3365
3543
  };
3366
3544
  }
3545
+ async callVision(model, systemPrompt, userPrompt, image, maxTokens) {
3546
+ const client = await this.getClient();
3547
+ const traceId = newLlmTraceId();
3548
+ const startedAt = Date.now();
3549
+ const resolvedModel = this.resolveModel(model);
3550
+ try {
3551
+ const response = await this.apiCall(
3552
+ () => client.models.generateContent({
3553
+ model: resolvedModel,
3554
+ contents: [
3555
+ {
3556
+ role: "user",
3557
+ parts: [
3558
+ { inlineData: { mimeType: image.mediaType, data: image.base64 } },
3559
+ { text: userPrompt }
3560
+ ]
3561
+ }
3562
+ ],
3563
+ config: {
3564
+ systemInstruction: systemPrompt,
3565
+ ...maxTokens ? { maxOutputTokens: maxTokens } : {}
3566
+ }
3567
+ })
3568
+ );
3569
+ const text = response.candidates?.[0]?.content?.parts?.map((p) => p.text || "").join("") || "";
3570
+ const usage = response.usageMetadata;
3571
+ const result = {
3572
+ text,
3573
+ inputTokens: usage?.promptTokenCount ?? 0,
3574
+ outputTokens: usage?.candidatesTokenCount ?? 0
3575
+ };
3576
+ const endedAt = Date.now();
3577
+ emitLlmTraceEvent({
3578
+ id: traceId,
3579
+ at: startedAt,
3580
+ endedAt,
3581
+ durationMs: endedAt - startedAt,
3582
+ provider: "google",
3583
+ model: resolvedModel,
3584
+ kind: "callVision",
3585
+ systemPrompt,
3586
+ userPrompt,
3587
+ result: {
3588
+ inputTokens: result.inputTokens,
3589
+ outputTokens: result.outputTokens,
3590
+ text
3591
+ }
3592
+ });
3593
+ return result;
3594
+ } catch (e) {
3595
+ const endedAt = Date.now();
3596
+ emitLlmTraceEvent({
3597
+ id: traceId,
3598
+ at: startedAt,
3599
+ endedAt,
3600
+ durationMs: endedAt - startedAt,
3601
+ provider: "google",
3602
+ model: resolvedModel,
3603
+ kind: "callVision",
3604
+ systemPrompt,
3605
+ userPrompt,
3606
+ error: { message: e instanceof Error ? e.message : String(e) }
3607
+ });
3608
+ throw e;
3609
+ }
3610
+ }
3367
3611
  async callTools(model, systemPrompt, userPrompt, tools, _toolChoice = "required", scopedTools) {
3368
3612
  const client = await this.getClient();
3369
3613
  const interactionTools = tools.map((t) => ({
@@ -3695,15 +3939,16 @@ var OpenRouterAgent = class extends LLMAgent {
3695
3939
  function createAgent(providerOrConfig) {
3696
3940
  const provider = typeof providerOrConfig === "string" ? providerOrConfig : providerOrConfig.provider;
3697
3941
  const routeOverride = typeof providerOrConfig === "string" ? void 0 : providerOrConfig.routeOverride;
3942
+ const onRetry = typeof providerOrConfig === "string" ? void 0 : providerOrConfig.onRetry;
3698
3943
  switch (provider) {
3699
3944
  case "anthropic":
3700
- return new AnthropicAgent(routeOverride);
3945
+ return new AnthropicAgent(routeOverride, onRetry);
3701
3946
  case "openai":
3702
- return new OpenAIAgent(routeOverride);
3947
+ return new OpenAIAgent(routeOverride, onRetry);
3703
3948
  case "google":
3704
- return new GoogleAgent(routeOverride);
3949
+ return new GoogleAgent(routeOverride, onRetry);
3705
3950
  case "openrouter":
3706
- return new OpenRouterAgent(routeOverride);
3951
+ return new OpenRouterAgent(routeOverride, onRetry);
3707
3952
  }
3708
3953
  }
3709
3954
  async function callLLM(config, systemPrompt, userPrompt, maxTokens) {