@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.
- package/dist/adapters/base.d.ts +7 -0
- package/dist/adapters/base.js +1 -0
- package/dist/adapters/base.js.map +1 -1
- package/dist/adapters/generation-dispatch.d.ts +17 -3
- package/dist/adapters/generation-dispatch.js +1078 -3464
- package/dist/adapters/generation-dispatch.js.map +1 -1
- package/dist/adapters/index.js +60 -3
- package/dist/adapters/index.js.map +1 -1
- package/dist/advanced/index.d.ts +4 -4
- package/dist/advanced/index.js +1346 -3557
- package/dist/advanced/index.js.map +1 -1
- package/dist/blueprint-validator.js +10 -3
- package/dist/blueprint-validator.js.map +1 -1
- package/dist/boilerplate.js +2 -2
- package/dist/boilerplate.js.map +1 -1
- package/dist/check/index.d.ts +1 -1
- package/dist/check/index.js.map +1 -1
- package/dist/{create-ui-generator-CxPvudbs.d.ts → create-ui-generator-BEkzEtNz.d.ts} +2 -2
- package/dist/{dispatch-N8wdNOX-.d.ts → dispatch-DJNpQ7DI.d.ts} +1 -1
- package/dist/evaluation/axis-checks/index.d.ts +3 -3
- package/dist/evaluation/axis-checks/index.js.map +1 -1
- package/dist/evaluation/axis-checks/registry.d.ts +1 -1
- package/dist/evaluation/index.d.ts +38 -6
- package/dist/evaluation/index.js +670 -3374
- package/dist/evaluation/index.js.map +1 -1
- package/dist/harness/check/runtime-render/index.d.ts +2 -2
- package/dist/harness/check/runtime-render/index.js +10 -3
- package/dist/harness/check/runtime-render/index.js.map +1 -1
- package/dist/harness/check/runtime-render/render-check-worker.js +5 -3
- package/dist/harness/check/runtime-render/render-check-worker.js.map +1 -1
- package/dist/harness/index.d.ts +2 -2
- package/dist/harness/index.js +587 -30
- package/dist/harness/index.js.map +1 -1
- package/dist/harness/llm-trace-sink.d.ts +7 -1
- package/dist/harness/llm-trace-sink.js.map +1 -1
- package/dist/harness/prompts.js +1 -1
- package/dist/harness/prompts.js.map +1 -1
- package/dist/harness/result-types.d.ts +15 -2
- package/dist/harness/result-types.js.map +1 -1
- package/dist/harness/runtime.d.ts +11 -2
- package/dist/harness/runtime.js +3 -3
- package/dist/harness/runtime.js.map +1 -1
- package/dist/harness/types-public.d.ts +1 -1
- package/dist/harness/types-public.js +2 -2
- package/dist/harness/types-public.js.map +1 -1
- package/dist/harness/validator-trace-sink.d.ts +1 -1
- package/dist/index.d.ts +23 -7
- package/dist/index.js +1346 -3557
- package/dist/index.js.map +1 -1
- package/dist/{llm-router-D7fjBhtr.d.ts → llm-router-DwwUK1bz.d.ts} +16 -3
- package/dist/patch.d.ts +1 -1
- package/dist/providers/index.js +5 -4
- package/dist/providers/index.js.map +1 -1
- package/dist/{render-check-WfOVO9kU.d.ts → render-check-C0CqVceF.d.ts} +17 -0
- package/dist/{types-public-DKR_h70H.d.ts → types-public-mM_-1Yk3.d.ts} +59 -1
- package/dist/validation/index.d.ts +1 -1
- package/dist/validation/index.js +1 -1
- package/dist/validation/index.js.map +1 -1
- package/dist/workflows.d.ts +1 -1
- package/package.json +16 -12
- package/src/adapters/base.ts +8 -0
- package/src/adapters/claude/claude-code-login.ts +128 -0
- package/src/adapters/claude/sdk.ts +9 -1
- package/src/adapters/generation-dispatch.ts +19 -2
- package/src/adapters/openai/tokens.ts +18 -0
- package/src/adapters/provider-router.ts +28 -0
- package/src/boilerplate/generate.ts +19 -4
- package/src/boilerplate/hard-sections.ts +1 -1
- package/src/boilerplate/templates/layouts/chat-mobile.tsx.tmpl +7 -3
- package/src/boilerplate/templates/layouts/chat-universal.tsx.tmpl +7 -3
- package/src/contract-context.ts +1 -1
- package/src/create-ui-generator.ts +49 -10
- package/src/evaluation/design-identity.ts +16 -5
- package/src/evaluation/index.ts +1 -0
- package/src/evaluation/llm-evaluator.ts +40 -13
- package/src/evaluation/types-public.ts +58 -0
- package/src/evaluation/visual-evaluator.ts +240 -30
- package/src/generator-build.ts +158 -0
- package/src/harness/check/runtime-render/adapter.ts +9 -0
- package/src/harness/check/runtime-render/render-check.ts +26 -5
- package/src/harness/claude-code-login-query.ts +529 -0
- package/src/harness/coding/assemble-result.ts +18 -2
- package/src/harness/coding/generate-task-runner.ts +75 -36
- package/src/harness/coding/run-coding-turn.ts +9 -0
- package/src/harness/coding/run-eval-round.ts +338 -38
- package/src/harness/enforced-coding.ts +20 -0
- package/src/harness/llm-router.ts +390 -55
- package/src/harness/llm-trace-sink.ts +7 -1
- package/src/harness/result-types.ts +14 -0
- package/src/harness/runtime.ts +6 -0
- package/src/index.ts +2 -1
- package/src/providers/openai.ts +31 -19
- 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
|
|
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
|
-
*
|
|
200
|
-
*
|
|
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
|
|
873
|
-
// `auto` for
|
|
874
|
-
// resulting zero-tool-call reply is never mistaken
|
|
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:
|
|
1137
|
-
outputTokens:
|
|
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:
|
|
1206
|
-
outputTokens:
|
|
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
|
-
|
|
1257
|
-
|
|
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
|
-
|
|
1396
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1629
|
-
|
|
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
|
-
|
|
1685
|
-
|
|
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:
|
|
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
|
-
|
|
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) */
|
package/src/harness/runtime.ts
CHANGED
|
@@ -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
|