@ggui-ai/ui-gen 0.20.0 → 0.22.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 (93) hide show
  1. package/dist/adapters/base.d.ts +7 -0
  2. package/dist/adapters/base.js +1 -0
  3. package/dist/adapters/base.js.map +1 -1
  4. package/dist/adapters/generation-dispatch.d.ts +17 -3
  5. package/dist/adapters/generation-dispatch.js +1078 -3464
  6. package/dist/adapters/generation-dispatch.js.map +1 -1
  7. package/dist/adapters/index.js +60 -3
  8. package/dist/adapters/index.js.map +1 -1
  9. package/dist/advanced/index.d.ts +4 -4
  10. package/dist/advanced/index.js +1346 -3557
  11. package/dist/advanced/index.js.map +1 -1
  12. package/dist/blueprint-validator.js +10 -3
  13. package/dist/blueprint-validator.js.map +1 -1
  14. package/dist/boilerplate.js +2 -2
  15. package/dist/boilerplate.js.map +1 -1
  16. package/dist/check/index.d.ts +1 -1
  17. package/dist/check/index.js.map +1 -1
  18. package/dist/{create-ui-generator-CxPvudbs.d.ts → create-ui-generator-BEkzEtNz.d.ts} +2 -2
  19. package/dist/{dispatch-N8wdNOX-.d.ts → dispatch-DJNpQ7DI.d.ts} +1 -1
  20. package/dist/evaluation/axis-checks/index.d.ts +3 -3
  21. package/dist/evaluation/axis-checks/index.js.map +1 -1
  22. package/dist/evaluation/axis-checks/registry.d.ts +1 -1
  23. package/dist/evaluation/index.d.ts +38 -6
  24. package/dist/evaluation/index.js +670 -3374
  25. package/dist/evaluation/index.js.map +1 -1
  26. package/dist/harness/check/runtime-render/index.d.ts +2 -2
  27. package/dist/harness/check/runtime-render/index.js +10 -3
  28. package/dist/harness/check/runtime-render/index.js.map +1 -1
  29. package/dist/harness/check/runtime-render/render-check-worker.js +5 -3
  30. package/dist/harness/check/runtime-render/render-check-worker.js.map +1 -1
  31. package/dist/harness/index.d.ts +2 -2
  32. package/dist/harness/index.js +587 -30
  33. package/dist/harness/index.js.map +1 -1
  34. package/dist/harness/llm-trace-sink.d.ts +7 -1
  35. package/dist/harness/llm-trace-sink.js.map +1 -1
  36. package/dist/harness/prompts.js +1 -1
  37. package/dist/harness/prompts.js.map +1 -1
  38. package/dist/harness/result-types.d.ts +15 -2
  39. package/dist/harness/result-types.js.map +1 -1
  40. package/dist/harness/runtime.d.ts +11 -2
  41. package/dist/harness/runtime.js +3 -3
  42. package/dist/harness/runtime.js.map +1 -1
  43. package/dist/harness/types-public.d.ts +1 -1
  44. package/dist/harness/types-public.js +2 -2
  45. package/dist/harness/types-public.js.map +1 -1
  46. package/dist/harness/validator-trace-sink.d.ts +1 -1
  47. package/dist/index.d.ts +23 -7
  48. package/dist/index.js +1346 -3557
  49. package/dist/index.js.map +1 -1
  50. package/dist/{llm-router-D7fjBhtr.d.ts → llm-router-DwwUK1bz.d.ts} +16 -3
  51. package/dist/patch.d.ts +1 -1
  52. package/dist/providers/index.js +5 -4
  53. package/dist/providers/index.js.map +1 -1
  54. package/dist/{render-check-WfOVO9kU.d.ts → render-check-C0CqVceF.d.ts} +17 -0
  55. package/dist/{types-public-DKR_h70H.d.ts → types-public-mM_-1Yk3.d.ts} +59 -1
  56. package/dist/validation/index.d.ts +1 -1
  57. package/dist/validation/index.js +1 -1
  58. package/dist/validation/index.js.map +1 -1
  59. package/dist/workflows.d.ts +1 -1
  60. package/package.json +16 -12
  61. package/src/adapters/base.ts +8 -0
  62. package/src/adapters/claude/claude-code-login.ts +128 -0
  63. package/src/adapters/claude/sdk.ts +9 -1
  64. package/src/adapters/generation-dispatch.ts +19 -2
  65. package/src/adapters/openai/tokens.ts +18 -0
  66. package/src/adapters/provider-router.ts +28 -0
  67. package/src/boilerplate/generate.ts +19 -4
  68. package/src/boilerplate/hard-sections.ts +1 -1
  69. package/src/boilerplate/templates/layouts/chat-mobile.tsx.tmpl +7 -3
  70. package/src/boilerplate/templates/layouts/chat-universal.tsx.tmpl +7 -3
  71. package/src/contract-context.ts +1 -1
  72. package/src/create-ui-generator.ts +49 -10
  73. package/src/evaluation/design-identity.ts +16 -5
  74. package/src/evaluation/index.ts +1 -0
  75. package/src/evaluation/llm-evaluator.ts +40 -13
  76. package/src/evaluation/types-public.ts +58 -0
  77. package/src/evaluation/visual-evaluator.ts +240 -30
  78. package/src/generator-build.ts +158 -0
  79. package/src/harness/check/runtime-render/adapter.ts +9 -0
  80. package/src/harness/check/runtime-render/render-check.ts +26 -5
  81. package/src/harness/claude-code-login-query.ts +529 -0
  82. package/src/harness/coding/assemble-result.ts +18 -2
  83. package/src/harness/coding/generate-task-runner.ts +75 -36
  84. package/src/harness/coding/run-coding-turn.ts +9 -0
  85. package/src/harness/coding/run-eval-round.ts +338 -38
  86. package/src/harness/enforced-coding.ts +20 -0
  87. package/src/harness/llm-router.ts +390 -55
  88. package/src/harness/llm-trace-sink.ts +7 -1
  89. package/src/harness/result-types.ts +14 -0
  90. package/src/harness/runtime.ts +6 -0
  91. package/src/index.ts +2 -1
  92. package/src/providers/openai.ts +31 -19
  93. package/src/validation/primitives.ts +1 -1
@@ -24,17 +24,33 @@ import type {
24
24
  GoogleGenAI,
25
25
  Interactions,
26
26
  } from '@google/genai';
27
- import type { JsonObject } from '@ggui-ai/protocol';
27
+ import {
28
+ anthropicRejectsForcedToolChoice,
29
+ anthropicRejectsSamplingParams,
30
+ normalizeAnthropicModelId,
31
+ type JsonObject,
32
+ } from '@ggui-ai/protocol';
28
33
  import type { LLMToolDef } from '../llm.js';
29
34
  import type { AnthropicBedrock } from '@anthropic-ai/bedrock-sdk';
30
35
  import { createAnthropicClient } from '../adapters/claude/client.js';
31
36
  import { getBedrockModelId } from '../adapters/provider-router.js';
32
37
  import { toolArgsToJsonObject } from '../adapters/tool-bridge.js';
38
+ import { splitCacheInclusiveUsage, splitOpenAiUsage } from '../adapters/openai/tokens.js';
33
39
  import {
34
40
  emitLlmTraceEvent,
35
41
  newLlmTraceId,
36
42
  summarizeTools,
37
43
  } from './llm-trace-sink.js';
44
+ import {
45
+ buildLoginQueryOptions,
46
+ collectLoginTurn,
47
+ createDenyingIntercept,
48
+ createLoginToolServer,
49
+ imageUserMessage,
50
+ LOGIN_REQUIRED_TOOL_INSTRUCTION,
51
+ type InterceptedToolCall,
52
+ type LoginSdk,
53
+ } from './claude-code-login-query.js';
38
54
 
39
55
  // `LLMToolDef` lives in `@ggui-ai/ui-gen/llm`. This file re-exports it
40
56
  // so `../harness/llm-router.js` importers (evaluator.ts,
@@ -76,6 +92,16 @@ export interface AgentConfig {
76
92
  readonly apiKey?: string;
77
93
  /** Anthropic-only: force the Bedrock-IAM client instead of the direct API client. */
78
94
  readonly useBedrock?: boolean;
95
+ /**
96
+ * Anthropic-only (ggui#1185): run on the machine's own Claude Code
97
+ * LOGIN instead of a key — the route decision `auth: 'claude-code-login'`
98
+ * lands here, and `createAgent` / `createVisionAgent` select
99
+ * `ClaudeCodeLoginAgent` (one `query()` per call) instead of the raw
100
+ * API client. Every agent the harness builds — coding, evaluation,
101
+ * visual judge — takes the same decision. Off unless set; a real key
102
+ * never coexists with it (the router refuses a non-anthropic route).
103
+ */
104
+ readonly claudeCodeLogin?: boolean;
79
105
  };
80
106
  /**
81
107
  * Observer invoked once per retried attempt when `apiCall()` (#489)
@@ -180,6 +206,8 @@ export interface LLMWithToolsResponse {
180
206
  inputTokens: number;
181
207
  outputTokens: number;
182
208
  turnsUsed: number;
209
+ /** Prompt-cache reads summed over the loop's turns, when the provider reports them (absent = unreported, never zero). */
210
+ cacheReadTokens?: number;
183
211
  }
184
212
 
185
213
  export interface LLMToolCall {
@@ -195,9 +223,10 @@ export interface LLMToolCallResponse {
195
223
  outputTokens: number;
196
224
  /**
197
225
  * The tool-choice mode the provider ACTUALLY received. Callers ask
198
- * for `'required'` (Anthropic `tool_choice: any`); Claude Fable 5.1
199
- * rejects forced tool choice with HTTP 400, so the Anthropic adapter
200
- * downgrades to `'auto'` for that family and reports it here. A
226
+ * for `'required'` (Anthropic `tool_choice: any`); Claude Fable 5.1 and
227
+ * Opus 5.5 reject forced tool choice with HTTP 400, so the Anthropic
228
+ * and OpenRouter adapters downgrade to `'auto'` for those families and
229
+ * report it here. A
201
230
  * downgraded call may legitimately return zero tool calls — the
202
231
  * evaluator's `criteriaCoverage` records that as `skipped`, never as
203
232
  * a pass. Absent = predates the field (treat as unknown).
@@ -546,39 +575,6 @@ export abstract class LLMAgent {
546
575
  // AnthropicAgent
547
576
  // =============================================================================
548
577
 
549
- /**
550
- * Strip every routing spelling down to the bare Claude API id so the
551
- * family predicates below see one form: `anthropic/claude-x`,
552
- * `anthropic.claude-x`, `us.anthropic.claude-x[-vN:M]` and the bare
553
- * `claude-x` all normalize to `claude-x…`. ARNs are left as-is (they
554
- * name a profile, not a family) and match no predicate.
555
- */
556
- export function normalizeAnthropicModelId(model: string): string {
557
- return model
558
- .replace(/^anthropic\//, '')
559
- .replace(/^(?:[a-z]{2}\.)?anthropic\./, '');
560
- }
561
-
562
- /**
563
- * Models that reject non-default sampling parameters with HTTP 400:
564
- * Opus 4.7 and everything after it (Fable 5, Fable 5.1, Opus 5,
565
- * Sonnet 5). Haiku 4.5 (`claude-haiku-4-5*`) still accepts them.
566
- * Strings per ggui#706 (platform.claude.com, verified 2026-09-02).
567
- */
568
- export function anthropicRejectsSamplingParams(model: string): boolean {
569
- return /^claude-(?:opus-4-7|opus-5|sonnet-5|fable-5)(?:-|$)/.test(
570
- normalizeAnthropicModelId(model),
571
- );
572
- }
573
-
574
- /**
575
- * Models that reject a FORCED tool choice (`tool_choice: any` / a named
576
- * tool) with HTTP 400: Claude Fable 5.1 (ggui#706). `auto` is accepted.
577
- */
578
- export function anthropicRejectsForcedToolChoice(model: string): boolean {
579
- return /^claude-fable-5-1(?:-|$)/.test(normalizeAnthropicModelId(model));
580
- }
581
-
582
578
  /**
583
579
  * Decide what sampling reaches Anthropic for `model` given the caller's
584
580
  * `temperature`: pass it through on models that accept it, strip it on
@@ -869,10 +865,10 @@ export class AnthropicAgent extends LLMAgent {
869
865
  // want the final-message shape downstream, so use the stream
870
866
  // helper's `finalMessage()` which folds the events back to the
871
867
  // same `Message` Type the non-streaming path returned.
872
- // Fable 5.1 rejects a forced tool choice (400); downgrade to
873
- // `auto` for that family and disclose it on the response so a
874
- // resulting zero-tool-call reply is never mistaken for a verdict
875
- // (ggui#710 / #706).
868
+ // Fable 5.1 and Opus 5.5 reject a forced tool choice (400);
869
+ // downgrade to `auto` for those families and disclose it on the
870
+ // response so a resulting zero-tool-call reply is never mistaken
871
+ // for a verdict (ggui#710 / #706 / #1254).
876
872
  const appliedToolChoice: 'required' | 'auto' =
877
873
  toolChoice === 'required' && anthropicRejectsForcedToolChoice(resolvedModel)
878
874
  ? 'auto'
@@ -1090,6 +1086,258 @@ export class AnthropicAgent extends LLMAgent {
1090
1086
  // OpenAIAgent
1091
1087
  // =============================================================================
1092
1088
 
1089
+ /**
1090
+ * ggui#1185 (B) — the Anthropic model reached through the machine's own
1091
+ * Claude Code LOGIN, for the one path that has no key: every router call
1092
+ * is ONE Agent SDK `query()` (`maxTurns: 1`), the binary PROPOSES tool
1093
+ * calls and ggui's harness executes them — the same one-shot contract as
1094
+ * `AnthropicAgent.callTools`, so everything above the router (the coding
1095
+ * loop, `apply_changes`, the axis checks, the evaluation legs) is
1096
+ * untouched. Selected only when the route decision carries
1097
+ * `routeOverride.claudeCodeLogin`; never by default.
1098
+ *
1099
+ * What it refuses, as behaviour (the carve-out): it writes no key
1100
+ * anywhere (the env the binary inherits is stripped of every provider key
1101
+ * name and `system:init.apiKeySource` must read `"none"` or the run stops
1102
+ * before its first turn); it never runs `--bare`, never loads `~/.claude`,
1103
+ * and never allow-lists a tool (an allow-listed MCP tool executes without
1104
+ * asking). The pure pieces — options, the JSON-Schema→Zod tool bridge, the
1105
+ * stream reader and its error classes — live in
1106
+ * `./claude-code-login-query.ts`; the measurements behind each are cited
1107
+ * there. Rate limits are the plan's: a 429 the binary relays rides the
1108
+ * same `apiCall()` retry seam as the raw client.
1109
+ *
1110
+ * Honest asymmetries, reported rather than hidden: the SDK has no
1111
+ * `tool_choice`, so `appliedToolChoice` is always `'auto'`; the binary
1112
+ * emitted a thinking block despite `thinkingConfig: { type: 'disabled' }`
1113
+ * in the probe (Exp 009 reports thinking tokens as their own column);
1114
+ * `warmCache` is a no-op (a warm-up turn would spend the plan for
1115
+ * nothing); `callWithTools` (an in-client agentic loop) is not offered —
1116
+ * the harness runs the loop.
1117
+ */
1118
+ export class ClaudeCodeLoginAgent extends LLMAgent implements VisionAgent {
1119
+ readonly provider = 'anthropic' as const;
1120
+
1121
+ protected resolveModel(model: string): string {
1122
+ return model.startsWith('anthropic/') ? model.slice('anthropic/'.length) : model;
1123
+ }
1124
+
1125
+ protected async createClient(): Promise<LoginSdk> {
1126
+ const { query, tool, createSdkMcpServer } = await import('@anthropic-ai/claude-agent-sdk');
1127
+ return { query, tool, createSdkMcpServer };
1128
+ }
1129
+
1130
+ async callText(
1131
+ model: string,
1132
+ systemPrompt: string,
1133
+ userPrompt: string,
1134
+ _maxTokens?: number,
1135
+ _temperature?: number,
1136
+ ): Promise<LLMResponse> {
1137
+ const sdk = await this.getClient<LoginSdk>();
1138
+ const traceId = newLlmTraceId();
1139
+ const startedAt = Date.now();
1140
+ const resolvedModel = this.resolveModel(model);
1141
+ try {
1142
+ const turn = await this.apiCall(() =>
1143
+ collectLoginTurn(
1144
+ sdk.query({
1145
+ prompt: userPrompt,
1146
+ options: buildLoginQueryOptions({ model: resolvedModel, systemPrompt }),
1147
+ }),
1148
+ { intercepted: [], kind: "callText", choice: "none", log: console.log, warn: console.warn },
1149
+ ),
1150
+ );
1151
+ const usage = turn.result.usage;
1152
+ const endedAt = Date.now();
1153
+ emitLlmTraceEvent({
1154
+ id: traceId,
1155
+ at: startedAt,
1156
+ endedAt,
1157
+ durationMs: endedAt - startedAt,
1158
+ provider: 'claude-code-login',
1159
+ model: resolvedModel,
1160
+ kind: 'callText',
1161
+ systemPrompt,
1162
+ userPrompt,
1163
+ result: {
1164
+ inputTokens: usage.input_tokens,
1165
+ outputTokens: usage.output_tokens,
1166
+ cacheCreated: usage.cache_creation_input_tokens,
1167
+ cacheRead: usage.cache_read_input_tokens,
1168
+ text: turn.text,
1169
+ },
1170
+ });
1171
+ return { text: turn.text, inputTokens: usage.input_tokens, outputTokens: usage.output_tokens };
1172
+ } catch (e) {
1173
+ const endedAt = Date.now();
1174
+ emitLlmTraceEvent({
1175
+ id: traceId,
1176
+ at: startedAt,
1177
+ endedAt,
1178
+ durationMs: endedAt - startedAt,
1179
+ provider: 'claude-code-login',
1180
+ model: resolvedModel,
1181
+ kind: 'callText',
1182
+ systemPrompt,
1183
+ userPrompt,
1184
+ error: { message: e instanceof Error ? e.message : String(e) },
1185
+ });
1186
+ throw e;
1187
+ }
1188
+ }
1189
+
1190
+ async callVision(
1191
+ model: string,
1192
+ systemPrompt: string,
1193
+ userPrompt: string,
1194
+ image: VisionImageInput,
1195
+ _maxTokens?: number,
1196
+ ): Promise<LLMResponse> {
1197
+ const sdk = await this.getClient<LoginSdk>();
1198
+ const traceId = newLlmTraceId();
1199
+ const startedAt = Date.now();
1200
+ const resolvedModel = this.resolveModel(model);
1201
+ try {
1202
+ const turn = await this.apiCall(() =>
1203
+ collectLoginTurn(
1204
+ sdk.query({
1205
+ prompt: imageUserMessage(userPrompt, image),
1206
+ options: buildLoginQueryOptions({ model: resolvedModel, systemPrompt }),
1207
+ }),
1208
+ { intercepted: [], kind: "callVision", choice: "none", log: console.log, warn: console.warn },
1209
+ ),
1210
+ );
1211
+ const usage = turn.result.usage;
1212
+ const endedAt = Date.now();
1213
+ emitLlmTraceEvent({
1214
+ id: traceId,
1215
+ at: startedAt,
1216
+ endedAt,
1217
+ durationMs: endedAt - startedAt,
1218
+ provider: 'claude-code-login',
1219
+ model: resolvedModel,
1220
+ kind: 'callVision',
1221
+ systemPrompt,
1222
+ userPrompt,
1223
+ result: { inputTokens: usage.input_tokens, outputTokens: usage.output_tokens, text: turn.text },
1224
+ });
1225
+ return { text: turn.text, inputTokens: usage.input_tokens, outputTokens: usage.output_tokens };
1226
+ } catch (e) {
1227
+ const endedAt = Date.now();
1228
+ emitLlmTraceEvent({
1229
+ id: traceId,
1230
+ at: startedAt,
1231
+ endedAt,
1232
+ durationMs: endedAt - startedAt,
1233
+ provider: 'claude-code-login',
1234
+ model: resolvedModel,
1235
+ kind: 'callVision',
1236
+ systemPrompt,
1237
+ userPrompt,
1238
+ error: { message: e instanceof Error ? e.message : String(e) },
1239
+ });
1240
+ throw e;
1241
+ }
1242
+ }
1243
+
1244
+ async callTools(
1245
+ model: string,
1246
+ systemPrompt: string,
1247
+ userPrompt: string,
1248
+ tools: LLMToolDef[],
1249
+ toolChoice: 'required' | 'auto' = 'required',
1250
+ _scopedTools?: LLMToolDef[],
1251
+ ): Promise<LLMToolCallResponse> {
1252
+ const sdk = await this.getClient<LoginSdk>();
1253
+ const traceId = newLlmTraceId();
1254
+ const startedAt = Date.now();
1255
+ const resolvedModel = this.resolveModel(model);
1256
+ // ggui#1273 — the Agent SDK has no tool_choice; 'required' is implemented
1257
+ // as rnd's instruction on the USER prompt (never the system prompt) and
1258
+ // disclosed as instructed-not-enforced: appliedToolChoice stays 'auto'.
1259
+ const sentUserPrompt =
1260
+ toolChoice === 'required' ? `${userPrompt}\n\n${LOGIN_REQUIRED_TOOL_INSTRUCTION}` : userPrompt;
1261
+ try {
1262
+ const turn = await this.apiCall(() => {
1263
+ // Fresh per attempt: the SDK connects the server instance per query().
1264
+ const intercepted: InterceptedToolCall[] = [];
1265
+ const server = createLoginToolServer(sdk, tools);
1266
+ return collectLoginTurn(
1267
+ sdk.query({
1268
+ prompt: sentUserPrompt,
1269
+ options: buildLoginQueryOptions({
1270
+ model: resolvedModel,
1271
+ systemPrompt,
1272
+ tools: { server, canUseTool: createDenyingIntercept(intercepted) },
1273
+ }),
1274
+ }),
1275
+ { intercepted, kind: "callTools", choice: toolChoice === 'required' ? "required:instructed" : "auto", log: console.log, warn: console.warn },
1276
+ );
1277
+ });
1278
+ const usage = turn.result.usage;
1279
+ const cacheCreated = usage.cache_creation_input_tokens;
1280
+ const cacheRead = usage.cache_read_input_tokens;
1281
+ if (cacheCreated || cacheRead) {
1282
+ console.log(
1283
+ `[claude-code-login] callTools cache: created=${cacheCreated} read=${cacheRead} input=${usage.input_tokens} output=${usage.output_tokens}`,
1284
+ );
1285
+ }
1286
+ const toolCalls: LLMToolCall[] = turn.toolCalls.map((c) => ({ id: c.id, name: c.name, input: c.input }));
1287
+ const endedAt = Date.now();
1288
+ emitLlmTraceEvent({
1289
+ id: traceId,
1290
+ at: startedAt,
1291
+ endedAt,
1292
+ durationMs: endedAt - startedAt,
1293
+ provider: 'claude-code-login',
1294
+ model: resolvedModel,
1295
+ kind: 'callTools',
1296
+ systemPrompt,
1297
+ userPrompt: sentUserPrompt,
1298
+ tools: summarizeTools(tools),
1299
+ result: {
1300
+ inputTokens: usage.input_tokens,
1301
+ outputTokens: usage.output_tokens,
1302
+ cacheCreated,
1303
+ cacheRead,
1304
+ toolCalls: toolCalls.map((c) => ({ name: c.name, input: c.input })),
1305
+ },
1306
+ });
1307
+ return {
1308
+ toolCalls,
1309
+ inputTokens: usage.input_tokens,
1310
+ outputTokens: usage.output_tokens,
1311
+ cacheReadTokens: cacheRead,
1312
+ cacheCreationTokens: cacheCreated,
1313
+ appliedToolChoice: 'auto',
1314
+ };
1315
+ } catch (e) {
1316
+ const endedAt = Date.now();
1317
+ emitLlmTraceEvent({
1318
+ id: traceId,
1319
+ at: startedAt,
1320
+ endedAt,
1321
+ durationMs: endedAt - startedAt,
1322
+ provider: 'claude-code-login',
1323
+ model: resolvedModel,
1324
+ kind: 'callTools',
1325
+ systemPrompt,
1326
+ userPrompt: sentUserPrompt,
1327
+ tools: summarizeTools(tools),
1328
+ error: { message: e instanceof Error ? e.message : String(e) },
1329
+ });
1330
+ throw e;
1331
+ }
1332
+ }
1333
+
1334
+ async callWithTools(): Promise<LLMWithToolsResponse> {
1335
+ throw new Error(
1336
+ "claude-code-login: callWithTools (an in-client agentic loop) is not part of the login client — ggui's harness runs the loop through callTools",
1337
+ );
1338
+ }
1339
+ }
1340
+
1093
1341
  export class OpenAIAgent extends LLMAgent {
1094
1342
  readonly provider = 'openai' as const;
1095
1343
 
@@ -1131,10 +1379,19 @@ export class OpenAIAgent extends LLMAgent {
1131
1379
  }
1132
1380
  }
1133
1381
 
1382
+ // ggui#1186 (second site): `input_tokens` INCLUDES the cached prefix on the
1383
+ // Responses API — report the NON-cached input like every other provider.
1384
+ // The text response carries no cache field, so the cached count is
1385
+ // dropped here rather than double-counted.
1386
+ const textUsage = splitOpenAiUsage(
1387
+ response.usage?.input_tokens ?? 0,
1388
+ response.usage?.output_tokens ?? 0,
1389
+ response.usage?.input_tokens_details?.cached_tokens ?? 0,
1390
+ );
1134
1391
  return {
1135
1392
  text,
1136
- inputTokens: response.usage?.input_tokens ?? 0,
1137
- outputTokens: response.usage?.output_tokens ?? 0,
1393
+ inputTokens: textUsage.tokens.input,
1394
+ outputTokens: textUsage.tokens.output,
1138
1395
  sampling: { temperature: temperature ?? 'provider-default' },
1139
1396
  };
1140
1397
  }
@@ -1196,14 +1453,26 @@ export class OpenAIAgent extends LLMAgent {
1196
1453
  (o): o is ResponseFunctionToolCall => o.type === 'function_call',
1197
1454
  );
1198
1455
 
1456
+ // ggui#1186 (second site): the harness coding loop reads THIS per-turn
1457
+ // usage (`run-coding-turn` → `generate-task-runner` telemetry), not the
1458
+ // adapters' results — so the split lives here too: `inputTokens` is the
1459
+ // NON-cached input and `cacheReadTokens` the cached subset, reported (0
1460
+ // when the breakdown says none), so a generation's metadata — and whatever
1461
+ // prices or aggregates it downstream — sees OpenAI cache reads at all.
1462
+ const toolsUsage = splitOpenAiUsage(
1463
+ response.usage?.input_tokens ?? 0,
1464
+ response.usage?.output_tokens ?? 0,
1465
+ response.usage?.input_tokens_details?.cached_tokens ?? 0,
1466
+ );
1199
1467
  return {
1200
1468
  toolCalls: functionCalls.map((fc) => ({
1201
1469
  id: fc.call_id,
1202
1470
  name: fc.name,
1203
1471
  input: JSON.parse(fc.arguments ?? '{}') as JsonObject,
1204
1472
  })),
1205
- inputTokens: response.usage?.input_tokens ?? 0,
1206
- outputTokens: response.usage?.output_tokens ?? 0,
1473
+ inputTokens: toolsUsage.tokens.input,
1474
+ outputTokens: toolsUsage.tokens.output,
1475
+ cacheReadTokens: toolsUsage.cacheReadTokens,
1207
1476
  };
1208
1477
  }
1209
1478
 
@@ -1242,6 +1511,7 @@ export class OpenAIAgent extends LLMAgent {
1242
1511
  ];
1243
1512
  let totalIn = 0;
1244
1513
  let totalOut = 0;
1514
+ let totalCacheRead = 0;
1245
1515
  let allText = '';
1246
1516
 
1247
1517
  for (let turn = 0; turn < maxTurns; turn++) {
@@ -1253,8 +1523,15 @@ export class OpenAIAgent extends LLMAgent {
1253
1523
  tools: openaiTools,
1254
1524
  }),
1255
1525
  );
1256
- totalIn += response.usage?.input_tokens ?? 0;
1257
- totalOut += response.usage?.output_tokens ?? 0;
1526
+ // ggui#1186 (second site): non-cached input + cached subset, per turn.
1527
+ const turnUsage = splitOpenAiUsage(
1528
+ response.usage?.input_tokens ?? 0,
1529
+ response.usage?.output_tokens ?? 0,
1530
+ response.usage?.input_tokens_details?.cached_tokens ?? 0,
1531
+ );
1532
+ totalIn += turnUsage.tokens.input;
1533
+ totalOut += turnUsage.tokens.output;
1534
+ totalCacheRead += turnUsage.cacheReadTokens;
1258
1535
 
1259
1536
  const functionCalls = response.output.filter(
1260
1537
  (o): o is ResponseFunctionToolCall => o.type === 'function_call',
@@ -1291,6 +1568,7 @@ export class OpenAIAgent extends LLMAgent {
1291
1568
  text: allText,
1292
1569
  inputTokens: totalIn,
1293
1570
  outputTokens: totalOut,
1571
+ cacheReadTokens: totalCacheRead,
1294
1572
  turnsUsed: input.length,
1295
1573
  };
1296
1574
  }
@@ -1339,6 +1617,19 @@ function toUserInputStep(text: string): Interactions.UserInputStep {
1339
1617
  return { type: 'user_input', content: [{ type: 'text', text }] };
1340
1618
  }
1341
1619
 
1620
+ /**
1621
+ * ggui#1186 (third site) — an Interactions `usage` split into the shared
1622
+ * convention: `total_input_tokens` includes the cached part of the prompt
1623
+ * (`total_cached_tokens`, per the SDK's own words), so the non-cached prompt
1624
+ * is the input and the cached subset is reported separately.
1625
+ */
1626
+ function googleInteractionUsage(
1627
+ usage: { total_input_tokens?: number; total_output_tokens?: number; total_cached_tokens?: number } | undefined,
1628
+ ): { inputTokens: number; outputTokens: number; cacheReadTokens: number } {
1629
+ const split = splitCacheInclusiveUsage(usage?.total_input_tokens ?? 0, usage?.total_output_tokens ?? 0, usage?.total_cached_tokens ?? 0);
1630
+ return { inputTokens: split.tokens.input, outputTokens: split.tokens.output, cacheReadTokens: split.cacheReadTokens };
1631
+ }
1632
+
1342
1633
  export class GoogleAgent extends LLMAgent {
1343
1634
  readonly provider = 'google' as const;
1344
1635
 
@@ -1392,8 +1683,9 @@ export class GoogleAgent extends LLMAgent {
1392
1683
 
1393
1684
  return {
1394
1685
  text: interactionText(interaction),
1395
- inputTokens: interaction.usage?.total_input_tokens ?? 0,
1396
- outputTokens: interaction.usage?.total_output_tokens ?? 0,
1686
+ // ggui#1186 (third site): `total_input_tokens` INCLUDES the cached part of the prompt — report the non-cached input.
1687
+ inputTokens: googleInteractionUsage(interaction.usage).inputTokens,
1688
+ outputTokens: googleInteractionUsage(interaction.usage).outputTokens,
1397
1689
  sampling: { temperature: temperature ?? 'provider-default' },
1398
1690
  };
1399
1691
  }
@@ -1435,7 +1727,8 @@ export class GoogleAgent extends LLMAgent {
1435
1727
  const usage = response.usageMetadata;
1436
1728
  const result: LLMResponse = {
1437
1729
  text,
1438
- inputTokens: usage?.promptTokenCount ?? 0,
1730
+ // ggui#1186 (third site): `promptTokenCount` includes `cachedContentTokenCount` — report the non-cached input.
1731
+ inputTokens: splitCacheInclusiveUsage(usage?.promptTokenCount ?? 0, usage?.candidatesTokenCount ?? 0, usage?.cachedContentTokenCount ?? 0).tokens.input,
1439
1732
  outputTokens: usage?.candidatesTokenCount ?? 0,
1440
1733
  };
1441
1734
 
@@ -1625,8 +1918,8 @@ export class GoogleAgent extends LLMAgent {
1625
1918
 
1626
1919
  return {
1627
1920
  toolCalls,
1628
- inputTokens: interaction.usage?.total_input_tokens ?? 0,
1629
- outputTokens: interaction.usage?.total_output_tokens ?? 0,
1921
+ // ggui#1186 (third site): the coding loop reads this per-turn usage — the non-cached prompt as input, the cached subset reported.
1922
+ ...googleInteractionUsage(interaction.usage),
1630
1923
  };
1631
1924
  }
1632
1925
 
@@ -1664,6 +1957,7 @@ export class GoogleAgent extends LLMAgent {
1664
1957
 
1665
1958
  let totalIn = 0;
1666
1959
  let totalOut = 0;
1960
+ let totalCacheRead = 0;
1667
1961
  let allText = '';
1668
1962
  let turnsUsed = 0;
1669
1963
 
@@ -1681,8 +1975,11 @@ export class GoogleAgent extends LLMAgent {
1681
1975
  turnsUsed = turn + 1;
1682
1976
 
1683
1977
  if (interaction.usage) {
1684
- totalIn += interaction.usage.total_input_tokens ?? 0;
1685
- totalOut += interaction.usage.total_output_tokens ?? 0;
1978
+ // ggui#1186 (third site): non-cached input + cached subset, per turn.
1979
+ const turnUsage = googleInteractionUsage(interaction.usage);
1980
+ totalIn += turnUsage.inputTokens;
1981
+ totalOut += turnUsage.outputTokens;
1982
+ totalCacheRead += turnUsage.cacheReadTokens;
1686
1983
  }
1687
1984
 
1688
1985
  // Collect text
@@ -1730,6 +2027,7 @@ export class GoogleAgent extends LLMAgent {
1730
2027
  text: allText,
1731
2028
  inputTokens: totalIn,
1732
2029
  outputTokens: totalOut,
2030
+ cacheReadTokens: totalCacheRead,
1733
2031
  turnsUsed,
1734
2032
  };
1735
2033
  }
@@ -1821,12 +2119,25 @@ export class OpenRouterAgent extends LLMAgent {
1821
2119
  }
1822
2120
  this.sessionMessages.push({ role: 'user', content: userPrompt });
1823
2121
 
2122
+ // ggui#1254 — OpenRouter forwards the tool choice to the upstream, so an
2123
+ // Anthropic family that refuses a forced tool choice (Fable 5.1, Opus 5.5;
2124
+ // OpenRouter spells them `anthropic/claude-opus-5.5`) 400s here exactly as
2125
+ // it does direct. Same guard, same disclosure as the Anthropic adapter.
2126
+ const requested = toolChoice ?? 'auto';
2127
+ const appliedToolChoice: 'required' | 'auto' =
2128
+ requested === 'required' && anthropicRejectsForcedToolChoice(resolved) ? 'auto' : requested;
2129
+ if (appliedToolChoice !== requested) {
2130
+ console.warn(
2131
+ `[openrouter] callTools: ${resolved} rejects forced tool_choice (HTTP 400); caller's 'required' downgraded to 'auto'`,
2132
+ );
2133
+ }
2134
+
1824
2135
  const response = await this.apiCall(() =>
1825
2136
  client.chatCompletion({
1826
2137
  model: resolved,
1827
2138
  messages: this.sessionMessages,
1828
2139
  tools: orTools,
1829
- tool_choice: toolChoice ?? 'auto',
2140
+ tool_choice: appliedToolChoice,
1830
2141
  }),
1831
2142
  );
1832
2143
 
@@ -1850,6 +2161,7 @@ export class OpenRouterAgent extends LLMAgent {
1850
2161
  toolCalls,
1851
2162
  inputTokens: response.usage.prompt_tokens,
1852
2163
  outputTokens: response.usage.completion_tokens,
2164
+ appliedToolChoice,
1853
2165
  };
1854
2166
  }
1855
2167
 
@@ -1962,6 +2274,14 @@ export function createAgent(providerOrConfig: AgentConfig['provider'] | AgentCon
1962
2274
  const routeOverride =
1963
2275
  typeof providerOrConfig === 'string' ? undefined : providerOrConfig.routeOverride;
1964
2276
  const onRetry = typeof providerOrConfig === 'string' ? undefined : providerOrConfig.onRetry;
2277
+ if (routeOverride?.claudeCodeLogin === true) {
2278
+ if (provider !== 'anthropic') {
2279
+ throw new Error(
2280
+ `${provider}: the claude-code-login credential authenticates the Claude Code binary only — pick an anthropic route or supply a ${provider} API key`,
2281
+ );
2282
+ }
2283
+ return new ClaudeCodeLoginAgent(routeOverride, onRetry);
2284
+ }
1965
2285
  switch (provider) {
1966
2286
  case 'anthropic':
1967
2287
  return new AnthropicAgent(routeOverride, onRetry);
@@ -1988,11 +2308,26 @@ export type VisionProvider = 'anthropic' | 'google';
1988
2308
  export function createVisionAgent(
1989
2309
  config: AgentConfig & { provider: VisionProvider },
1990
2310
  ): LLMAgent & VisionAgent {
2311
+ if (config.routeOverride?.claudeCodeLogin === true) {
2312
+ if (config.provider !== 'anthropic') {
2313
+ throw new Error(
2314
+ `${config.provider}: the claude-code-login credential authenticates the Claude Code binary only — pick an anthropic route or supply a ${config.provider} API key`,
2315
+ );
2316
+ }
2317
+ return new ClaudeCodeLoginAgent(config.routeOverride, config.onRetry);
2318
+ }
1991
2319
  switch (config.provider) {
1992
2320
  case 'anthropic':
1993
2321
  return new AnthropicAgent(config.routeOverride, config.onRetry);
1994
2322
  case 'google':
1995
2323
  return new GoogleAgent(config.routeOverride, config.onRetry);
2324
+ default: {
2325
+ // ggui#1248 — refused by NAME, never answered with `undefined`: a caller that
2326
+ // asserted its way past the type got back an agent it could not call, and the
2327
+ // throw surfaced three frames later as "reading 'callVision'".
2328
+ const unreachable: never = config.provider;
2329
+ throw new Error(`createVisionAgent: provider '${String(unreachable)}' has no vision path (anthropic, google)`);
2330
+ }
1996
2331
  }
1997
2332
  }
1998
2333
 
@@ -32,7 +32,13 @@
32
32
  import type { LLMToolDef } from '../llm.js';
33
33
 
34
34
  /** Provider tag that produced the call. Mirrors {@link AgentConfig.provider}. */
35
- export type LlmTraceProvider = 'anthropic' | 'openai' | 'google' | 'openrouter';
35
+ /**
36
+ * `claude-code-login` is the Anthropic model reached through the machine's
37
+ * own Claude Code login (ggui#1185) rather than a key — a trace reader can
38
+ * tell the two apart, which the routing vocabulary (`provider: 'anthropic'`)
39
+ * deliberately does not.
40
+ */
41
+ export type LlmTraceProvider = 'anthropic' | 'claude-code-login' | 'openai' | 'google' | 'openrouter';
36
42
 
37
43
  /** Which `LLMAgent` method initiated the call. */
38
44
  export type LlmTraceKind =
@@ -49,11 +49,25 @@ export type ModelRoles = Partial<Record<ModelRole, string>>;
49
49
  // Generation Result
50
50
  // =============================================================================
51
51
 
52
+ /**
53
+ * The same-exchange guard (ggui#404) ended this generation: the coding model
54
+ * made one identical tool exchange (same tool, same input, same result)
55
+ * `repeats` times, so the loop stopped instead of burning its remaining turns.
56
+ * Present ONLY when that guard ended the run — whether anything shipped is
57
+ * the compiled code's to say (the loop falls back to an earlier build).
58
+ */
59
+ export interface SameExchangeBreak {
60
+ readonly tool: string;
61
+ readonly repeats: number;
62
+ }
63
+
52
64
  export interface GenerationResult extends AdapterResult {
53
65
  /** Number of generation passes the harness performed */
54
66
  passesUsed: number;
55
67
  /** Three-tier evaluation result (tier 0 + LLM tier 1+2 + visual) */
56
68
  evalResult?: EvalResult;
69
+ /** Set only when the same-exchange guard ended the run (ggui#404); no key otherwise. */
70
+ sameExchangeBreak?: SameExchangeBreak;
57
71
  /** Whether background improvement should be spawned (auto-improve mode) */
58
72
  needsBackgroundImprovement?: boolean;
59
73
  /** Whether at least one commit passed the self-check (compile + type check + lint) */
@@ -91,6 +91,12 @@ export interface SingleComponentParams {
91
91
  * today's single screenshot at `viewport`.
92
92
  */
93
93
  canvases?: readonly CanvasClass[];
94
+ /**
95
+ * ggui#1195 — per-canvas viewport override for the in-loop round: the
96
+ * box the composer targeted (`rendering.viewport`) so the round judges
97
+ * the same box. See `VisualEvalConfig.canvasViewports`.
98
+ */
99
+ canvasViewports?: Partial<Readonly<Record<CanvasClass, { width: number; height: number }>>>;
94
100
  /**
95
101
  * The stylesheet the in-loop visual round renders under (a caller's
96
102
  * theme composed by `cssTokensForAppTheme`); absent = the design's