@ggui-ai/ui-gen 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/generation-dispatch.d.ts +18 -1
- package/dist/adapters/generation-dispatch.js +819 -1577
- package/dist/adapters/generation-dispatch.js.map +1 -1
- package/dist/adapters/index.js +28 -11
- package/dist/adapters/index.js.map +1 -1
- package/dist/advanced/index.d.ts +5 -3
- package/dist/advanced/index.js +838 -1579
- package/dist/advanced/index.js.map +1 -1
- package/dist/blueprint-validator.js +109 -3
- package/dist/blueprint-validator.js.map +1 -1
- package/dist/boilerplate.js +17 -4
- package/dist/boilerplate.js.map +1 -1
- package/dist/check/index.d.ts +1 -1
- package/dist/check/index.js +11 -1
- package/dist/check/index.js.map +1 -1
- package/dist/compose.js +5 -5
- package/dist/compose.js.map +1 -1
- package/dist/{create-ui-generator-GX7tvbyH.d.ts → create-ui-generator-CF0EL0Li.d.ts} +26 -1
- package/dist/{dispatch-Deo9qzrG.d.ts → dispatch-D75M-OfZ.d.ts} +1 -1
- package/dist/evaluation/axis-checks/index.d.ts +3 -3
- package/dist/evaluation/axis-checks/index.js +133 -1
- package/dist/evaluation/axis-checks/index.js.map +1 -1
- package/dist/evaluation/axis-checks/registry.d.ts +1 -1
- package/dist/evaluation/axis-checks/registry.js +133 -1
- package/dist/evaluation/axis-checks/registry.js.map +1 -1
- package/dist/evaluation/index.d.ts +46 -3
- package/dist/evaluation/index.js +410 -24
- package/dist/evaluation/index.js.map +1 -1
- package/dist/fragments/index.js +5 -5
- package/dist/fragments/index.js.map +1 -1
- package/dist/harness/check/runtime-render/index.d.ts +2 -2
- package/dist/harness/check/runtime-render/index.js +110 -3
- package/dist/harness/check/runtime-render/index.js.map +1 -1
- package/dist/harness/index.d.ts +5 -126
- package/dist/harness/index.js +292 -32
- package/dist/harness/index.js.map +1 -1
- package/dist/harness/llm-trace-sink.d.ts +1 -1
- package/dist/harness/llm-trace-sink.js.map +1 -1
- package/dist/harness/prompts.js +17 -3
- package/dist/harness/prompts.js.map +1 -1
- package/dist/harness/result-types.d.ts +1 -1
- package/dist/harness/runtime.d.ts +17 -1
- package/dist/harness/runtime.js +23 -8
- package/dist/harness/runtime.js.map +1 -1
- package/dist/harness/types-public.d.ts +1 -1
- package/dist/harness/types-public.js +22 -9
- package/dist/harness/types-public.js.map +1 -1
- package/dist/harness/validator-trace-sink.d.ts +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +838 -1579
- package/dist/index.js.map +1 -1
- package/dist/llm-router-BEVtbAb9.d.ts +213 -0
- package/dist/patch.d.ts +1 -1
- package/dist/provider-adapter.d.ts +16 -6
- package/dist/provider-adapter.js.map +1 -1
- package/dist/providers/index.d.ts +54 -99
- package/dist/providers/index.js +13 -8
- package/dist/providers/index.js.map +1 -1
- package/dist/{render-check-CNZjHD5g.d.ts → render-check-BNYGVDJd.d.ts} +1 -1
- package/dist/{types-public-C36k1q6s.d.ts → types-public-DIpjC-OA.d.ts} +1 -1
- package/dist/validation/index.js +2 -2
- package/dist/validation/index.js.map +1 -1
- package/dist/workflows.d.ts +1 -1
- package/package.json +18 -18
- package/src/adapters/generation-dispatch.ts +28 -2
- package/src/adapters/openrouter/client.ts +2 -2
- package/src/adapters/openrouter/types.ts +10 -0
- package/src/boilerplate/generate.ts +10 -2
- package/src/boilerplate/json-schema-ts.ts +22 -2
- package/src/boilerplate/system-prompt.ts +4 -2
- package/src/compile.ts +1 -1
- package/src/contract-context.ts +10 -2
- package/src/create-ui-generator.ts +58 -11
- package/src/design-system-docs.ts +6 -4
- package/src/evaluation/axis-checks/checks/state-merge.ts +1 -1
- package/src/evaluation/axis-checks/checks/state-ui-affordance.ts +162 -0
- package/src/evaluation/axis-checks/checks/universal.ts +39 -0
- package/src/evaluation/axis-checks/registry.ts +2 -0
- package/src/evaluation/index.ts +11 -0
- package/src/evaluation/llm-evaluator.ts +26 -3
- package/src/evaluation/visual-evaluator.ts +49 -66
- package/src/fragments/realtime.ts +3 -3
- package/src/fragments/state.ts +2 -2
- package/src/harness/check/runtime-render/adapter.ts +18 -0
- package/src/harness/check/runtime-render/index.ts +8 -0
- package/src/harness/check/runtime-render/prepare-mockup.ts +14 -0
- package/src/harness/check/runtime-render/production-act-shim.ts +158 -0
- package/src/harness/check/runtime-render/render-check.ts +72 -1
- package/src/harness/coding/init-session.ts +4 -1
- package/src/harness/coding/run-eval-round.ts +67 -5
- package/src/harness/index.ts +1 -1
- package/src/harness/llm-router.ts +451 -27
- package/src/harness/llm-trace-sink.ts +2 -1
- package/src/harness/runtime.ts +15 -0
- package/src/index.ts +5 -5
- package/src/provider-adapter.ts +16 -6
- package/src/providers/bedrock.ts +89 -26
- package/src/providers/index.ts +16 -4
- package/src/tools/render-check.ts +19 -7
- package/src/validation/ui-compiler.ts +2 -2
package/dist/harness/index.d.ts
CHANGED
|
@@ -1,138 +1,17 @@
|
|
|
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
|
-
|
|
5
|
-
import { LLMToolDef } from '../llm.js';
|
|
4
|
+
export { A as AgentConfig, L as LLMResponse, P as ProviderRetryInfo, c as callLLM, a as createAgent } from '../llm-router-BEVtbAb9.js';
|
|
6
5
|
export { WORKFLOWS, WorkflowId, pickWorkflow } from '../workflows.js';
|
|
7
6
|
export { LlmTraceEvent, LlmTraceKind, LlmTraceProvider, LlmTraceSink, getLlmTraceSink, setLlmTraceSink } from './llm-trace-sink.js';
|
|
8
7
|
export { ValidatorTraceEvent, ValidatorTraceSink, getValidatorTraceSink, setValidatorTraceSink } from './validator-trace-sink.js';
|
|
9
8
|
export { ProcessMode } from '../policy.js';
|
|
10
9
|
export { computeHarnessId, computeHarnessName, hashClassification } from '../hash.js';
|
|
10
|
+
import '@ggui-ai/protocol';
|
|
11
11
|
import '../axes-CzLEMDeB.js';
|
|
12
|
-
import '../types-public-
|
|
12
|
+
import '../types-public-DIpjC-OA.js';
|
|
13
13
|
import '../types-BOvHNG7K.js';
|
|
14
|
-
|
|
15
|
-
interface AgentConfig {
|
|
16
|
-
provider: 'anthropic' | 'openai' | 'google' | 'openrouter';
|
|
17
|
-
model: string;
|
|
18
|
-
/**
|
|
19
|
-
* Sampling temperature. When defined it is threaded into the provider
|
|
20
|
-
* request; when undefined the provider default is used (unchanged
|
|
21
|
-
* behavior for all existing callers).
|
|
22
|
-
*/
|
|
23
|
-
temperature?: number;
|
|
24
|
-
}
|
|
25
|
-
interface LLMResponse {
|
|
26
|
-
text: string;
|
|
27
|
-
inputTokens: number;
|
|
28
|
-
outputTokens: number;
|
|
29
|
-
}
|
|
30
|
-
interface LLMTool {
|
|
31
|
-
name: string;
|
|
32
|
-
description: string;
|
|
33
|
-
parameters: JsonObject;
|
|
34
|
-
handler: (args: JsonObject) => Promise<{
|
|
35
|
-
content: Array<{
|
|
36
|
-
text: string;
|
|
37
|
-
}>;
|
|
38
|
-
isError?: boolean;
|
|
39
|
-
}>;
|
|
40
|
-
}
|
|
41
|
-
interface LLMWithToolsResponse {
|
|
42
|
-
text: string;
|
|
43
|
-
inputTokens: number;
|
|
44
|
-
outputTokens: number;
|
|
45
|
-
turnsUsed: number;
|
|
46
|
-
}
|
|
47
|
-
interface LLMToolCall {
|
|
48
|
-
/** Provider-specific call ID (for sendToolResult) */
|
|
49
|
-
id?: string;
|
|
50
|
-
name: string;
|
|
51
|
-
input: JsonObject;
|
|
52
|
-
}
|
|
53
|
-
interface LLMToolCallResponse {
|
|
54
|
-
toolCalls: LLMToolCall[];
|
|
55
|
-
inputTokens: number;
|
|
56
|
-
outputTokens: number;
|
|
57
|
-
/**
|
|
58
|
-
* Tokens read from / written to the prompt cache, when the provider
|
|
59
|
-
* reports them. Optional because not every provider exposes
|
|
60
|
-
* prompt-cache accounting — absent means "unreported", never zero.
|
|
61
|
-
*/
|
|
62
|
-
cacheReadTokens?: number;
|
|
63
|
-
cacheCreationTokens?: number;
|
|
64
|
-
}
|
|
65
|
-
/** Result of executing a tool — passed to sendToolResult to close the API contract. */
|
|
66
|
-
interface LLMToolResult {
|
|
67
|
-
/** Tool call ID from the response (for providers that need it) */
|
|
68
|
-
callId?: string;
|
|
69
|
-
/** Tool name */
|
|
70
|
-
name: string;
|
|
71
|
-
/** Text result of executing the tool */
|
|
72
|
-
result: string;
|
|
73
|
-
/** Whether the tool execution failed */
|
|
74
|
-
isError?: boolean;
|
|
75
|
-
}
|
|
76
|
-
declare abstract class LLMAgent {
|
|
77
|
-
abstract readonly provider: AgentConfig['provider'];
|
|
78
|
-
private client;
|
|
79
|
-
protected lastSessionId: string | undefined;
|
|
80
|
-
protected abstract resolveModel(model: string): string;
|
|
81
|
-
protected abstract createClient(): Promise<unknown>;
|
|
82
|
-
protected getClient<T>(): Promise<T>;
|
|
83
|
-
/** Text-only call — no tools */
|
|
84
|
-
abstract callText(model: string, systemPrompt: string, userPrompt: string, maxTokens?: number, temperature?: number): Promise<LLMResponse>;
|
|
85
|
-
/**
|
|
86
|
-
* Single-turn function calling — returns tool calls without executing them.
|
|
87
|
-
* Each provider uses its native function/tool calling:
|
|
88
|
-
* - Anthropic: tool_use blocks
|
|
89
|
-
* - OpenAI: function_call output items
|
|
90
|
-
* - Google: functionCall parts
|
|
91
|
-
* SDK handles JSON escaping — safe for code, diffs, and other content.
|
|
92
|
-
*/
|
|
93
|
-
abstract callTools(model: string, systemPrompt: string, userPrompt: string, tools: LLMToolDef[], toolChoice?: 'required' | 'auto',
|
|
94
|
-
/**
|
|
95
|
-
* Optional scoped fallback tools. If the primary tools fail with a
|
|
96
|
-
* transport-class error (e.g. `malformed_tool_call` on Gemini after
|
|
97
|
-
* retry exhaustion), the provider may retry once with these narrower
|
|
98
|
-
* tools before throwing. Universal signal — not provider-gated.
|
|
99
|
-
*/
|
|
100
|
-
scopedTools?: LLMToolDef[]): Promise<LLMToolCallResponse>;
|
|
101
|
-
/** Multi-turn agentic loop — executes tools internally */
|
|
102
|
-
abstract callWithTools(model: string, systemPrompt: string, userPrompt: string, tools: LLMTool[], maxTurns: number): Promise<LLMWithToolsResponse>;
|
|
103
|
-
/**
|
|
104
|
-
* Pre-warm cache for repeated callTools() calls with the same system prompt + tools.
|
|
105
|
-
* Override in providers that support server-side context caching (e.g., Google).
|
|
106
|
-
* No-op by default (Anthropic/OpenAI handle caching automatically per-request).
|
|
107
|
-
*/
|
|
108
|
-
warmCache(_model: string, _systemPrompt: string, _tools: LLMToolDef[], _toolChoice?: 'required' | 'auto'): Promise<void>;
|
|
109
|
-
/** Cleanup any cached resources. Call after generation completes. No-op by default. */
|
|
110
|
-
cleanup(): Promise<void>;
|
|
111
|
-
/** Reset session state between independent generation runs. */
|
|
112
|
-
resetSession(): void;
|
|
113
|
-
/**
|
|
114
|
-
* Send tool execution results back to the provider to close the API contract.
|
|
115
|
-
* Call this after executing tools from callTools() and before the next callTools().
|
|
116
|
-
*
|
|
117
|
-
* For providers with server-side state (Google, OpenAI), this sends the
|
|
118
|
-
* function results so the next callTools() can chain properly.
|
|
119
|
-
* For stateless providers (Anthropic), this is a no-op.
|
|
120
|
-
*
|
|
121
|
-
* Override in providers that need it.
|
|
122
|
-
*/
|
|
123
|
-
sendToolResult(_results: LLMToolResult[]): Promise<void>;
|
|
124
|
-
/**
|
|
125
|
-
* Execute an API call. No retry — if it fails, it fails.
|
|
126
|
-
* Logs the error with provider context and re-throws.
|
|
127
|
-
*/
|
|
128
|
-
protected apiCall<T>(fn: () => Promise<T>): Promise<T>;
|
|
129
|
-
}
|
|
130
|
-
/**
|
|
131
|
-
* Create a fresh agent instance for the given provider.
|
|
132
|
-
* Each call returns a new instance — no shared state between callers.
|
|
133
|
-
*/
|
|
134
|
-
declare function createAgent(provider: AgentConfig['provider']): LLMAgent;
|
|
135
|
-
declare function callLLM(config: AgentConfig, systemPrompt: string, userPrompt: string, maxTokens?: number): Promise<LLMResponse>;
|
|
14
|
+
import '../llm.js';
|
|
136
15
|
|
|
137
16
|
/**
|
|
138
17
|
* Fingerprint a materialized Harness. `h.id` is already a deterministic
|
|
@@ -142,4 +21,4 @@ declare function callLLM(config: AgentConfig, systemPrompt: string, userPrompt:
|
|
|
142
21
|
*/
|
|
143
22
|
declare function hashHarness(h: Harness): string;
|
|
144
23
|
|
|
145
|
-
export {
|
|
24
|
+
export { Harness, hashHarness };
|
package/dist/harness/index.js
CHANGED
|
@@ -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);
|
|
@@ -517,11 +519,11 @@ var stateFragments = {
|
|
|
517
519
|
axis: "state",
|
|
518
520
|
value: "merge",
|
|
519
521
|
cacheTier: "axisDelta",
|
|
520
|
-
promptText: "## State: merge (live entity reconciliation)\n1. Seed `const [items, setItems] = useState(props.items ?? [])
|
|
522
|
+
promptText: "## State: merge (live entity reconciliation)\n1. Seed `const [items, setItems] = useState(props.items ?? [])` AND re-seed on prop change: `useEffect(() => setItems(props.items ?? []), [props.items])` \u2014 a `ggui_amend` repaint must move the list.\n2. On stream updates: `setItems(prev => prev.map(it => it.id === event.id ? { ...it, ...event } : it))` \u2014 merge by id, do NOT append.\n3. Memoize derived views (grouping/sorting/filtering) with useMemo.\n4. Per-item actions pass `{ id, ... }` in the payload.\n5. Never push the stream payload into an append-only list unless realtime=append.",
|
|
521
523
|
boilerplateMarker: [
|
|
522
524
|
"",
|
|
523
525
|
" // \u2500\u2500 Live entity state (merge-by-id) \u2500\u2500",
|
|
524
|
-
" // useState(props.items ?? []); merge stream events by item.id; never append.",
|
|
526
|
+
" // useState(props.items ?? []); re-seed via useEffect on [props.items]; merge stream events by item.id; never append.",
|
|
525
527
|
""
|
|
526
528
|
].join("\n")
|
|
527
529
|
},
|
|
@@ -639,13 +641,13 @@ var realtimeFragments = {
|
|
|
639
641
|
axis: "realtime",
|
|
640
642
|
value: "merge",
|
|
641
643
|
cacheTier: "axisDelta",
|
|
642
|
-
promptText: "## Realtime: merge\nStream events carry an id; merge into LOCAL STATE. Subscribing alone is not enough \u2014 the local list must update on each event for the DOM to re-render.\n\n```tsx\nconst stream = useStream<UpdateT>('streamName');\nconst [items, setItems] = useState(props.items);\nuseEffect(() => {\n if (!stream.latest) return;\n setItems((prev) => prev.map((it) => it.id === stream.latest!.id ? { ...it, ...stream.latest } : it));\n}, [stream.latest]);\n// Render `items.map(...)` \u2014 NOT props.items, NOT stream.all.\n```\n\nNever append \u2014 stream=merge means the entity already exists locally."
|
|
644
|
+
promptText: "## Realtime: merge\nStream events carry an id; merge into LOCAL STATE. Subscribing alone is not enough \u2014 the local list must update on each event for the DOM to re-render.\n\n```tsx\nconst stream = useStream<UpdateT>('streamName');\nconst [items, setItems] = useState(props.items);\nuseEffect(() => setItems(props.items), [props.items]); // re-seed on ggui_amend repaint \u2014 props stay the source of truth\nuseEffect(() => {\n if (!stream.latest) return;\n setItems((prev) => prev.map((it) => it.id === stream.latest!.id ? { ...it, ...stream.latest } : it));\n}, [stream.latest]);\n// Render `items.map(...)` \u2014 NOT props.items, NOT stream.all.\n```\n\nNever append \u2014 stream=merge means the entity already exists locally."
|
|
643
645
|
},
|
|
644
646
|
append: {
|
|
645
647
|
axis: "realtime",
|
|
646
648
|
value: "append",
|
|
647
649
|
cacheTier: "axisDelta",
|
|
648
|
-
promptText: "## Realtime: append\nStream events are new entities; APPEND to local state. Subscribing alone is not enough \u2014 the local list must update for the DOM to re-render.\n\n```tsx\nconst stream = useStream<EventT>('streamName');\nconst [items, setItems] = useState(props.items);\nuseEffect(() => {\n if (!stream.latest) return;\n setItems((prev) => [...prev, stream.latest!]);\n}, [stream.latest]);\n// Render `items.map(...)` \u2014 NOT props.items, NOT stream.all (which accumulates only post-mount).\n```\n\nUse head for newest-first, tail for chat-style. Cap list length if needed. Do not dedupe unless the contract guarantees at-most-once."
|
|
650
|
+
promptText: "## Realtime: append\nStream events are new entities; APPEND to local state. Subscribing alone is not enough \u2014 the local list must update for the DOM to re-render.\n\n```tsx\nconst stream = useStream<EventT>('streamName');\nconst [items, setItems] = useState(props.items);\nuseEffect(() => setItems(props.items), [props.items]); // re-seed on ggui_amend repaint \u2014 props stay the source of truth\nuseEffect(() => {\n if (!stream.latest) return;\n setItems((prev) => [...prev, stream.latest!]);\n}, [stream.latest]);\n// Render `items.map(...)` \u2014 NOT props.items, NOT stream.all (which accumulates only post-mount).\n```\n\nUse head for newest-first, tail for chat-style. Cap list length if needed. Do not dedupe unless the contract guarantees at-most-once."
|
|
649
651
|
},
|
|
650
652
|
status: {
|
|
651
653
|
axis: "realtime",
|
|
@@ -663,7 +665,7 @@ var realtimeFragments = {
|
|
|
663
665
|
axis: "realtime",
|
|
664
666
|
value: "mixed",
|
|
665
667
|
cacheTier: "axisDelta",
|
|
666
|
-
promptText: "## Realtime: mixed\nMultiple stream channels with DIFFERENT semantics. Each `streamKinds` entry in the contract maps to one of: merge, append, status, presence. Subscribe + sync to local state PER CHANNEL \u2014 subscribing alone never re-renders the DOM.\n\nExample (chat-interface \u2014 message=append, typing=presence):\n\n```tsx\nconst messages = useStream<MessageT>('message');\nconst typing = useStream<TypingT>('typing');\nconst [messageList, setMessageList] = useState(props.messages);\nconst [typingUsers, setTypingUsers] = useState<Set<string>>(new Set());\n\nuseEffect(() => {\n if (!messages.latest) return;\n setMessageList((prev) => [...prev, messages.latest!]);\n}, [messages.latest]);\n\nuseEffect(() => {\n if (!typing.latest) return;\n setTypingUsers((prev) => {\n const next = new Set(prev);\n if (typing.latest!.active) next.add(typing.latest!.sender);\n else next.delete(typing.latest!.sender);\n return next;\n });\n}, [typing.latest]);\n```\n\nDo NOT treat all events the same. Do NOT skip the local-state sync \u2014 `useStream` alone won't trigger DOM updates of your derived view.",
|
|
668
|
+
promptText: "## Realtime: mixed\nMultiple stream channels with DIFFERENT semantics. Each `streamKinds` entry in the contract maps to one of: merge, append, status, presence. Subscribe + sync to local state PER CHANNEL \u2014 subscribing alone never re-renders the DOM.\n\nExample (chat-interface \u2014 message=append, typing=presence):\n\n```tsx\nconst messages = useStream<MessageT>('message');\nconst typing = useStream<TypingT>('typing');\nconst [messageList, setMessageList] = useState(props.messages);\nconst [typingUsers, setTypingUsers] = useState<Set<string>>(new Set());\n\nuseEffect(() => setMessageList(props.messages), [props.messages]); // re-seed on ggui_amend repaint\n\nuseEffect(() => {\n if (!messages.latest) return;\n setMessageList((prev) => [...prev, messages.latest!]);\n}, [messages.latest]);\n\nuseEffect(() => {\n if (!typing.latest) return;\n setTypingUsers((prev) => {\n const next = new Set(prev);\n if (typing.latest!.active) next.add(typing.latest!.sender);\n else next.delete(typing.latest!.sender);\n return next;\n });\n}, [typing.latest]);\n```\n\nDo NOT treat all events the same. Do NOT skip the local-state sync \u2014 `useStream` alone won't trigger DOM updates of your derived view.",
|
|
667
669
|
boilerplateMarker: [
|
|
668
670
|
"",
|
|
669
671
|
" // \u2500\u2500 Mixed stream handlers \u2500\u2500",
|
|
@@ -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
|
-
|
|
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
|
|
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
|
}
|
|
@@ -1785,13 +1798,15 @@ A \`DataContract\` declares everything a GguiSession exchanges with the outside
|
|
|
1785
1798
|
|
|
1786
1799
|
## Defensive coding for absent / late-arriving data
|
|
1787
1800
|
|
|
1788
|
-
Props arrive
|
|
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\`.
|
|
1793
1807
|
- **Stream latest**: \`useStream\` returns \`{latest: T | null, all: T[], isComplete: boolean}\`. The default \`all\` is \`[]\` so it's safe to map; \`latest\` is null until the first frame arrives \u2014 guard before reading \`.foo\`.
|
|
1794
1808
|
- **Stream reconciliation**: when a stream event carries an \`action\` discriminant (e.g. \`create | move | edit | delete\`), the channel is a CRUD feed \u2014 your handler MUST branch on EVERY value: append on \`create\`, drop on \`delete\`, replace-by-id on \`move\` / \`edit\`. Merging only the "edit" case silently loses created and deleted items. Reconcile into the SAME state that seeds from \`props\` (e.g. \`useState(() => props.tasks ?? [])\`) so the seed data and the live feed render as one list \u2014 and handle an event whose id is not yet present (a \`create\` for an unknown item) by inserting it, not ignoring it.
|
|
1809
|
+
- **Props re-sync on repaint**: props the agent owns are the source of truth for display \u2014 prefer rendering DIRECTLY from \`props\` (controlled, no local copy). When local optimistic state genuinely needs a prop seed (a click bumps a count before the agent confirms), it MUST re-sync on prop change \u2014 \`useEffect(() => setCount(props.count ?? 0), [props.count])\` \u2014 or a \`ggui_amend\` repaint applies the new props while the display keeps the stale local value. BAD: \`const [count, setCount] = useState(props.count ?? 0)\` alone. GOOD: the same seed + the one-line re-sync effect.
|
|
1795
1810
|
- **Loading state**: while data is still absent, render \`<Skeleton>\` placeholders \u2014 never a blank screen. \`<Skeleton variant="text" />\` for a text line, \`variant="circle"\` for an avatar slot, default \`rect\` for a block.
|
|
1796
1811
|
- **Empty state**: when a list or results array is empty, render \`<EmptyState title="\u2026" description="\u2026" />\` \u2014 a region that renders nothing when empty looks broken to the user.
|
|
1797
1812
|
|
|
@@ -1919,7 +1934,7 @@ Every value change is mirrored to the host LLM's context automatically (debounce
|
|
|
1919
1934
|
|
|
1920
1935
|
**When NOT to use it.** Local UI state the contract did NOT declare \u2014 \`isDropdownOpen\`, hover flags, animation phase, ephemeral toggles. For those, use a plain \`useState\` directly. The runtime ignores undeclared state.
|
|
1921
1936
|
|
|
1922
|
-
**\`contextSpec\` direction is one-way: client \u2192 agent.** The agent uses \`propsSpec\` (via \`ggui_update\`) and \`streamSpec\` (via the live channel) to push state TO the client. Don't try to write to the agent via \`contextSpec\` \u2014 there is no return path.
|
|
1937
|
+
**\`contextSpec\` direction is one-way: client \u2192 agent.** The agent uses \`propsSpec\` (via \`ggui_amend\` for in-place repaints of this mount, or \`ggui_update\` when it mints a new history card) and \`streamSpec\` (via the live channel) to push state TO the client. Don't try to write to the agent via \`contextSpec\` \u2014 there is no return path.
|
|
1923
1938
|
|
|
1924
1939
|
**Schema mismatches drop silently.** If you set a value that doesn't match the slot's schema (e.g. a string into a \`{type: 'number'}\` slot), the runtime logs a dev \`console.warn\` and skips the post. Make sure your setter calls produce values that match the declared shape.
|
|
1925
1940
|
|
|
@@ -2815,6 +2830,34 @@ function errorSummary(e) {
|
|
|
2815
2830
|
}
|
|
2816
2831
|
return `${e.constructor.name}${status}${code}: ${e.message.slice(0, 120)}${body}`;
|
|
2817
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;
|
|
2818
2861
|
var LLMAgent = class {
|
|
2819
2862
|
client = null;
|
|
2820
2863
|
// ── Session state for server-side chaining ──────────────
|
|
@@ -2822,6 +2865,19 @@ var LLMAgent = class {
|
|
|
2822
2865
|
// Enables turn 2+ to skip re-sending system prompt + history.
|
|
2823
2866
|
// Call resetSession() between independent generation runs.
|
|
2824
2867
|
lastSessionId;
|
|
2868
|
+
/**
|
|
2869
|
+
* Explicit per-call routing override — see `AgentConfig.routeOverride`.
|
|
2870
|
+
* Absent when constructed via the bare `new XAgent()` form (every
|
|
2871
|
+
* existing call site), so `createClient()`/`resolveModel()` fall
|
|
2872
|
+
* back to `process.env` exactly as before.
|
|
2873
|
+
*/
|
|
2874
|
+
routeOverride;
|
|
2875
|
+
/** See `AgentConfig.onRetry` (#489). Absent is a no-op. */
|
|
2876
|
+
onRetry;
|
|
2877
|
+
constructor(routeOverride, onRetry) {
|
|
2878
|
+
this.routeOverride = routeOverride;
|
|
2879
|
+
this.onRetry = onRetry;
|
|
2880
|
+
}
|
|
2825
2881
|
async getClient() {
|
|
2826
2882
|
if (!this.client) {
|
|
2827
2883
|
this.client = await this.createClient();
|
|
@@ -2855,36 +2911,104 @@ var LLMAgent = class {
|
|
|
2855
2911
|
async sendToolResult(_results) {
|
|
2856
2912
|
}
|
|
2857
2913
|
/**
|
|
2858
|
-
* Execute an API call
|
|
2859
|
-
*
|
|
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.
|
|
2860
2942
|
*/
|
|
2861
2943
|
async apiCall(fn) {
|
|
2862
2944
|
const start = Date.now();
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
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
|
+
}
|
|
2869
2991
|
}
|
|
2870
2992
|
}
|
|
2871
2993
|
};
|
|
2872
2994
|
var AnthropicAgent = class extends LLMAgent {
|
|
2873
2995
|
provider = "anthropic";
|
|
2874
2996
|
resolveModel(model) {
|
|
2875
|
-
|
|
2997
|
+
const useBedrock = this.routeOverride?.useBedrock ?? process.env.CLAUDE_CODE_USE_BEDROCK === "1";
|
|
2998
|
+
if (useBedrock) {
|
|
2876
2999
|
return getBedrockModelId(model);
|
|
2877
3000
|
}
|
|
2878
3001
|
return model.startsWith("anthropic/") ? model.slice("anthropic/".length) : model;
|
|
2879
3002
|
}
|
|
2880
3003
|
async createClient() {
|
|
2881
|
-
|
|
3004
|
+
const useBedrock = this.routeOverride?.useBedrock ?? process.env.CLAUDE_CODE_USE_BEDROCK === "1";
|
|
3005
|
+
if (useBedrock) {
|
|
2882
3006
|
const { AnthropicBedrock } = await import('@anthropic-ai/bedrock-sdk');
|
|
2883
3007
|
return new AnthropicBedrock({
|
|
2884
3008
|
awsRegion: process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION ?? "us-east-1"
|
|
2885
3009
|
});
|
|
2886
3010
|
}
|
|
2887
|
-
return createAnthropicClient(process.env.ANTHROPIC_API_KEY);
|
|
3011
|
+
return createAnthropicClient(this.routeOverride?.apiKey ?? process.env.ANTHROPIC_API_KEY);
|
|
2888
3012
|
}
|
|
2889
3013
|
async callText(model, systemPrompt, userPrompt, maxTokens, temperature) {
|
|
2890
3014
|
const client = await this.getClient();
|
|
@@ -2956,6 +3080,73 @@ var AnthropicAgent = class extends LLMAgent {
|
|
|
2956
3080
|
throw e;
|
|
2957
3081
|
}
|
|
2958
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
|
+
}
|
|
2959
3150
|
async callTools(model, systemPrompt, userPrompt, tools, toolChoice = "required", _scopedTools) {
|
|
2960
3151
|
const client = await this.getClient();
|
|
2961
3152
|
const traceId = newLlmTraceId();
|
|
@@ -3155,7 +3346,7 @@ var OpenAIAgent = class extends LLMAgent {
|
|
|
3155
3346
|
}
|
|
3156
3347
|
async createClient() {
|
|
3157
3348
|
const { default: OpenAISDK } = await import('openai');
|
|
3158
|
-
return new OpenAISDK({ apiKey: process.env.OPENAI_API_KEY });
|
|
3349
|
+
return new OpenAISDK({ apiKey: this.routeOverride?.apiKey ?? process.env.OPENAI_API_KEY });
|
|
3159
3350
|
}
|
|
3160
3351
|
async callText(model, systemPrompt, userPrompt, maxTokens, temperature) {
|
|
3161
3352
|
const client = await this.getClient();
|
|
@@ -3326,7 +3517,7 @@ var GoogleAgent = class extends LLMAgent {
|
|
|
3326
3517
|
async createClient() {
|
|
3327
3518
|
const { GoogleGenAI: GoogleGenAISDK } = await import('@google/genai');
|
|
3328
3519
|
return new GoogleGenAISDK({
|
|
3329
|
-
apiKey: process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY,
|
|
3520
|
+
apiKey: this.routeOverride?.apiKey ?? (process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY),
|
|
3330
3521
|
httpOptions: { timeout: 3e5 }
|
|
3331
3522
|
// 5 min — Pro models can be slow
|
|
3332
3523
|
});
|
|
@@ -3351,6 +3542,72 @@ var GoogleAgent = class extends LLMAgent {
|
|
|
3351
3542
|
outputTokens: interaction.usage?.total_output_tokens ?? 0
|
|
3352
3543
|
};
|
|
3353
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
|
+
}
|
|
3354
3611
|
async callTools(model, systemPrompt, userPrompt, tools, _toolChoice = "required", scopedTools) {
|
|
3355
3612
|
const client = await this.getClient();
|
|
3356
3613
|
const interactionTools = tools.map((t) => ({
|
|
@@ -3553,7 +3810,7 @@ var OpenRouterAgent = class extends LLMAgent {
|
|
|
3553
3810
|
async createClient() {
|
|
3554
3811
|
const { OpenRouterClient: OpenRouterClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
3555
3812
|
return new OpenRouterClient2({
|
|
3556
|
-
apiKey: process.env.OPENROUTER_API_KEY
|
|
3813
|
+
apiKey: this.routeOverride?.apiKey ?? process.env.OPENROUTER_API_KEY
|
|
3557
3814
|
});
|
|
3558
3815
|
}
|
|
3559
3816
|
async callText(model, systemPrompt, userPrompt, maxTokens, temperature) {
|
|
@@ -3679,21 +3936,24 @@ var OpenRouterAgent = class extends LLMAgent {
|
|
|
3679
3936
|
return { text: finalText, inputTokens: totalInput, outputTokens: totalOutput, turnsUsed };
|
|
3680
3937
|
}
|
|
3681
3938
|
};
|
|
3682
|
-
function createAgent(
|
|
3939
|
+
function createAgent(providerOrConfig) {
|
|
3940
|
+
const provider = typeof providerOrConfig === "string" ? providerOrConfig : providerOrConfig.provider;
|
|
3941
|
+
const routeOverride = typeof providerOrConfig === "string" ? void 0 : providerOrConfig.routeOverride;
|
|
3942
|
+
const onRetry = typeof providerOrConfig === "string" ? void 0 : providerOrConfig.onRetry;
|
|
3683
3943
|
switch (provider) {
|
|
3684
3944
|
case "anthropic":
|
|
3685
|
-
return new AnthropicAgent();
|
|
3945
|
+
return new AnthropicAgent(routeOverride, onRetry);
|
|
3686
3946
|
case "openai":
|
|
3687
|
-
return new OpenAIAgent();
|
|
3947
|
+
return new OpenAIAgent(routeOverride, onRetry);
|
|
3688
3948
|
case "google":
|
|
3689
|
-
return new GoogleAgent();
|
|
3949
|
+
return new GoogleAgent(routeOverride, onRetry);
|
|
3690
3950
|
case "openrouter":
|
|
3691
|
-
return new OpenRouterAgent();
|
|
3951
|
+
return new OpenRouterAgent(routeOverride, onRetry);
|
|
3692
3952
|
}
|
|
3693
3953
|
}
|
|
3694
3954
|
async function callLLM(config, systemPrompt, userPrompt, maxTokens) {
|
|
3695
3955
|
const start = Date.now();
|
|
3696
|
-
const agent = createAgent(config
|
|
3956
|
+
const agent = createAgent(config);
|
|
3697
3957
|
const result = await agent.callText(
|
|
3698
3958
|
config.model,
|
|
3699
3959
|
systemPrompt,
|