@ggui-ai/protocol 0.1.0-rc.1 → 0.2.0-alpha.1

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 (92) hide show
  1. package/README.md +3 -3
  2. package/dist/bridge/invoke-agent.d.ts +8 -8
  3. package/dist/bridge/invoke-agent.d.ts.map +1 -1
  4. package/dist/bridge/invoke-agent.js +12 -12
  5. package/dist/envelopes/builders.d.ts +2 -4
  6. package/dist/envelopes/builders.d.ts.map +1 -1
  7. package/dist/envelopes/builders.js +2 -6
  8. package/dist/gadgets/stdlib-gadgets.d.ts +1 -1
  9. package/dist/gadgets/stdlib-gadgets.js +1 -1
  10. package/dist/index.d.ts +2 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +4 -3
  13. package/dist/integrations/mcp-apps.d.ts +354 -711
  14. package/dist/integrations/mcp-apps.d.ts.map +1 -1
  15. package/dist/integrations/mcp-apps.js +266 -208
  16. package/dist/recommended-prompts.d.ts +1 -1
  17. package/dist/recommended-prompts.js +1 -1
  18. package/dist/schemas/data-contract.d.ts +4 -3
  19. package/dist/schemas/data-contract.d.ts.map +1 -1
  20. package/dist/schemas/data-contract.js +4 -3
  21. package/dist/schemas/invoke.d.ts +2 -2
  22. package/dist/schemas/invoke.js +2 -2
  23. package/dist/schemas/mcp.d.ts +60 -79
  24. package/dist/schemas/mcp.d.ts.map +1 -1
  25. package/dist/schemas/mcp.js +93 -116
  26. package/dist/schemas/sync-check.js +1 -1
  27. package/dist/stream/stream-parser.d.ts +4 -4
  28. package/dist/stream/stream-parser.d.ts.map +1 -1
  29. package/dist/stream/stream-parser.js +9 -9
  30. package/dist/transport/websocket.d.ts +14 -49
  31. package/dist/transport/websocket.d.ts.map +1 -1
  32. package/dist/types/app-config.d.ts +17 -32
  33. package/dist/types/app-config.d.ts.map +1 -1
  34. package/dist/types/canvas-lifecycle.d.ts +13 -13
  35. package/dist/types/canvas-lifecycle.d.ts.map +1 -1
  36. package/dist/types/canvas-lifecycle.js +3 -3
  37. package/dist/types/contract-inference.d.ts +4 -7
  38. package/dist/types/contract-inference.d.ts.map +1 -1
  39. package/dist/types/data-bindings.d.ts +1 -1
  40. package/dist/types/data-bindings.d.ts.map +1 -1
  41. package/dist/types/data-contract.d.ts +10 -10
  42. package/dist/types/data-contract.d.ts.map +1 -1
  43. package/dist/types/events.d.ts +14 -56
  44. package/dist/types/events.d.ts.map +1 -1
  45. package/dist/types/events.js +1 -8
  46. package/dist/types/feedback.d.ts +2 -4
  47. package/dist/types/feedback.d.ts.map +1 -1
  48. package/dist/types/handshake-suggestion.d.ts +1 -1
  49. package/dist/types/host-context.d.ts +5 -15
  50. package/dist/types/host-context.d.ts.map +1 -1
  51. package/dist/types/host-context.js +1 -1
  52. package/dist/types/live-channel.d.ts +128 -191
  53. package/dist/types/live-channel.d.ts.map +1 -1
  54. package/dist/types/llm-route.d.ts +233 -0
  55. package/dist/types/llm-route.d.ts.map +1 -0
  56. package/dist/types/llm-route.js +433 -0
  57. package/dist/types/llm.d.ts +35 -17
  58. package/dist/types/llm.d.ts.map +1 -1
  59. package/dist/types/llm.js +50 -39
  60. package/dist/types/mcp.d.ts +47 -137
  61. package/dist/types/mcp.d.ts.map +1 -1
  62. package/dist/types/mcp.js +2 -2
  63. package/dist/types/openrouter-models.d.ts +3 -2
  64. package/dist/types/openrouter-models.d.ts.map +1 -1
  65. package/dist/types/render-event.d.ts +119 -0
  66. package/dist/types/render-event.d.ts.map +1 -0
  67. package/dist/types/render-event.js +38 -0
  68. package/dist/types/session.d.ts +216 -342
  69. package/dist/types/session.d.ts.map +1 -1
  70. package/dist/types/ui-generator.d.ts +8 -9
  71. package/dist/types/ui-generator.d.ts.map +1 -1
  72. package/dist/validation/contract-validator.d.ts +60 -4
  73. package/dist/validation/contract-validator.d.ts.map +1 -1
  74. package/dist/validation/contract-validator.js +101 -5
  75. package/dist/validation/reserved-channels.d.ts +1 -1
  76. package/dist/validation/reserved-channels.js +5 -5
  77. package/dist/validation/sanitize-error.js +1 -1
  78. package/dist/validation/schema-compat-invariants.d.ts +2 -2
  79. package/dist/validation/schema-compat-invariants.js +2 -2
  80. package/dist/validation/schema-meta-validation.d.ts +1 -1
  81. package/dist/validation/schema-meta-validation.js +2 -2
  82. package/dist/validation/schema-subset.d.ts +1 -1
  83. package/dist/version.d.ts +253 -46
  84. package/dist/version.d.ts.map +1 -1
  85. package/dist/version.js +253 -46
  86. package/package.json +17 -16
  87. package/dist/navigation/index.d.ts +0 -3
  88. package/dist/navigation/index.d.ts.map +0 -1
  89. package/dist/navigation/index.js +0 -1
  90. package/dist/navigation/stack-navigation.d.ts +0 -55
  91. package/dist/navigation/stack-navigation.d.ts.map +0 -1
  92. package/dist/navigation/stack-navigation.js +0 -80
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Typed `LlmRoute` system — the structural-correctness replacement for
3
+ * string-typed model identifiers.
4
+ *
5
+ * Two concepts, one type:
6
+ * - `provider` — the API endpoint you authenticate against, owns its
7
+ * own model namespace. Includes marketplaces (Bedrock, OpenRouter)
8
+ * and direct-author APIs (Claude/Anthropic, OpenAI, Google AI
9
+ * Studio). NOT a separate "platform" dimension — each provider's
10
+ * API surface IS the platform.
11
+ * - `model` — the wire-canonical string for THIS provider. Registry
12
+ * KEY == what goes on the HTTP wire to the provider. No
13
+ * transformation at dispatch — what you write in `MODELS[provider]`
14
+ * is exactly what the API sees.
15
+ *
16
+ * Why this exists: three bugs of the same class in one week (#22 CLI
17
+ * sent slash-prefixed `google/gemini-3.5-flash` → Gemini 404; #42
18
+ * mcp-server negotiator sent `anthropic/claude-haiku-4-5` → Anthropic
19
+ * 404; the next one would have been...). The pattern was always
20
+ * "someone wrote a code path that bypassed `getUpstreamModelId`". The
21
+ * typed-route system makes the bug class structurally impossible —
22
+ * `LlmRoute` is a discriminated union, the dispatch is
23
+ * exhaustiveness-checked, and there's no transformation step to
24
+ * forget because the model string IS the wire form.
25
+ *
26
+ * Slice spec: `docs/plans/2026-05-25-llm-route-typed-system.md`
27
+ */
28
+ /**
29
+ * Wire-canonical model names per provider. The KEY for each entry is
30
+ * EXACTLY what the provider's API expects on the wire — no
31
+ * transformation, no prefix-strip, no map. Register a new model by
32
+ * adding its wire-canonical string to the matching provider's array.
33
+ *
34
+ * Provider naming uses the COMPANY name for consistency across all
35
+ * providers (`anthropic`, `openai`, `google`) — matches LiteLLM's
36
+ * `anthropic/` prefix + the existing `ANTHROPIC_API_KEY` env var
37
+ * convention. Marketplaces (`bedrock`, `openrouter`) keep their
38
+ * platform name because that IS the company you authenticate against.
39
+ *
40
+ * - `anthropic` — Anthropic's direct API
41
+ * (`api.anthropic.com`). Auth: `ANTHROPIC_API_KEY`.
42
+ * - `openai` — OpenAI's direct API.
43
+ * - `google` — Google AI Studio (`generativelanguage.googleapis.com`).
44
+ * - `bedrock` — AWS Bedrock marketplace. Region prefix (`us.`,
45
+ * `eu.`, `apac.`) is part of the wire name; each region is its
46
+ * own registry entry, not a `{region}` field.
47
+ * - `openrouter` — OpenRouter marketplace. Authors are sub-namespaced
48
+ * in the model string (`<author>/<model>`).
49
+ * - `vertex` — DEFERRED to its own slice. Vertex needs region +
50
+ * projectId + GCP IAM setup; migration is purely additive when
51
+ * ready.
52
+ */
53
+ export declare const MODELS: {
54
+ readonly anthropic: readonly ["claude-haiku-4-5-20251001", "claude-sonnet-4-6", "claude-opus-4-7", "claude-opus-4-6"];
55
+ readonly openai: readonly ["gpt-5.5", "gpt-5.5-2026-04-23", "gpt-5.5-pro", "gpt-5.5-pro-2026-04-23", "gpt-5.4", "gpt-5.4-2026-03-05", "gpt-5.4-mini", "gpt-5.4-mini-2026-03-17", "gpt-5.4-nano", "gpt-5.4-nano-2026-03-17", "gpt-5.3-codex"];
56
+ readonly google: readonly ["gemini-3.5-flash", "gemini-flash-latest", "gemini-3.1-flash-lite", "gemini-3.1-pro-preview", "gemini-3.1-flash-lite-preview", "gemini-3-flash-preview"];
57
+ readonly bedrock: readonly ["us.anthropic.claude-haiku-4-5-20251001-v1:0", "eu.anthropic.claude-haiku-4-5-20251001-v1:0", "apac.anthropic.claude-haiku-4-5-20251001-v1:0", "global.anthropic.claude-haiku-4-5-20251001-v1:0", "us.anthropic.claude-sonnet-4-6", "eu.anthropic.claude-sonnet-4-6", "apac.anthropic.claude-sonnet-4-6", "us.anthropic.claude-opus-4-7", "eu.anthropic.claude-opus-4-7", "apac.anthropic.claude-opus-4-7", "global.anthropic.claude-opus-4-7", "us.anthropic.claude-opus-4-6-v1"];
58
+ readonly openrouter: readonly ["anthropic/claude-haiku-4.5", "anthropic/claude-sonnet-4.6", "anthropic/claude-opus-4.7", "openai/gpt-5.5", "openai/gpt-5.5-pro", "openai/gpt-5.4-mini", "openai/gpt-5.4-nano", "google/gemini-3.5-flash", "google/gemini-3.1-pro-preview", "google/gemini-3.1-flash-lite", "x-ai/grok-4.3", "meta-llama/llama-4-maverick", "meta-llama/llama-3.3-70b-instruct", "deepseek/deepseek-v4-pro", "deepseek/deepseek-r1-0528", "qwen/qwen3.7-max", "qwen/qwen3-coder", "mistralai/mistral-large-2512", "openai/gpt-oss-120b"];
59
+ };
60
+ /**
61
+ * Every supported LLM provider. Adding a new provider = add a key to
62
+ * `MODELS` + add a dispatch case wherever `LlmRoute` is consumed (TS
63
+ * exhaustiveness check forces handling).
64
+ */
65
+ export type LlmProvider = keyof typeof MODELS;
66
+ /**
67
+ * Model names known at compile time for a given provider. For
68
+ * OpenRouter, this is the enumerated subset; for every other provider,
69
+ * this is the full set (their model lists are small + stable).
70
+ */
71
+ export type KnownModelOf<P extends LlmProvider> = (typeof MODELS)[P][number];
72
+ /**
73
+ * Model names accepted on a route for a given provider. Two providers
74
+ * use the `(string & {})` escape hatch:
75
+ *
76
+ * - **OpenRouter** — `<author>/<model>` permutation space is too
77
+ * large to enumerate. Curated subset gives IDE autocomplete;
78
+ * arbitrary strings pass validation via shape (matches the
79
+ * `<author>/<model>` rule).
80
+ * - **Bedrock** — operators pick from AWS-supplied foundation model
81
+ * ids (e.g. `'anthropic.claude-sonnet-4-6'`), cross-region
82
+ * inference profile ids (e.g. `'us.anthropic.claude-haiku-4-5-
83
+ * 20251001-v1:0'`), or even custom inference profile ARNs
84
+ * (`'arn:aws:bedrock:...'`). The MODELS.bedrock list curates the
85
+ * common cross-region profiles for autocomplete; arbitrary
86
+ * strings pass at runtime so operators with custom profiles +
87
+ * non-curated foundation models stay supported.
88
+ *
89
+ * Every other provider is strict-enum: only names in
90
+ * `MODELS[provider]` typecheck.
91
+ *
92
+ * The `(string & {})` trick preserves IDE autocomplete on the known
93
+ * subset while still accepting arbitrary strings — without it, the
94
+ * union collapses to `string` and the known entries lose autocomplete.
95
+ */
96
+ export type ModelOf<P extends LlmProvider> = P extends 'openrouter' | 'bedrock' ? KnownModelOf<P> | (string & {}) : KnownModelOf<P>;
97
+ /**
98
+ * A typed LLM call target. The pair `(provider, model)` is sufficient
99
+ * — `provider` selects the SDK / endpoint / auth scheme, `model` is
100
+ * the wire string that SDK sends literally.
101
+ *
102
+ * Discriminated union via mapped type: `LlmRoute` is the union of
103
+ * `{provider: P, model: ModelOf<P>}` for every `P`. TypeScript
104
+ * enforces that the model belongs to the provider's namespace — e.g.
105
+ * `{provider: 'bedrock', model: 'claude-haiku-4-5-20251001'}` is a
106
+ * compile error (that model lives under `claude`, not `bedrock`).
107
+ *
108
+ * Vertex AI is deferred — it needs `region` and `projectId` fields
109
+ * for endpoint construction + IAM. When added, the Vertex variant
110
+ * will look like `{provider: 'vertex', model: ..., region: ...,
111
+ * projectId: ...}` — purely additive, doesn't disturb existing routes.
112
+ */
113
+ export type LlmRoute = {
114
+ [P in LlmProvider]: {
115
+ provider: P;
116
+ model: ModelOf<P>;
117
+ };
118
+ }[LlmProvider];
119
+ /**
120
+ * Runtime type guard for the provider enum. Pairs with the parser at
121
+ * wire boundaries (ggui.json, CLI flags, env vars) where the value
122
+ * arrives as an unvalidated string.
123
+ */
124
+ export declare function isLlmProvider(s: string): s is LlmProvider;
125
+ /**
126
+ * Runtime type guard for a known model string under a given provider.
127
+ * Strict-enum check; does NOT accept OpenRouter's arbitrary-string
128
+ * extension — for that, use {@link isValidOpenrouterModel}.
129
+ */
130
+ export declare function isKnownModel<P extends LlmProvider>(provider: P, model: string): model is KnownModelOf<P>;
131
+ /**
132
+ * Validate an OpenRouter model string by shape: `<author>/<model>`
133
+ * where both segments are non-empty and contain only the characters
134
+ * OpenRouter's catalog uses (alphanumerics + `-` + `.` + `_` + `:`).
135
+ *
136
+ * Used for the OpenRouter escape hatch — strings that pass this check
137
+ * are accepted into `LlmRoute` even if not in `MODELS.openrouter[]`.
138
+ * Strings that fail it are rejected at the parser boundary.
139
+ */
140
+ export declare function isValidOpenrouterModel(s: string): boolean;
141
+ /**
142
+ * Validate a Bedrock model string by shape. AWS accepts THREE forms:
143
+ *
144
+ * - Cross-region inference profile ids — `<region>.<inner>` where
145
+ * region is `us`/`eu`/`apac`/`global` and inner contains `.`,
146
+ * `-`, `:`, alphanumerics (e.g.
147
+ * `'us.anthropic.claude-haiku-4-5-20251001-v1:0'`).
148
+ * - Bedrock foundation model ids — bare `<vendor>.<model>` form
149
+ * (e.g. `'anthropic.claude-sonnet-4-6'`).
150
+ * - Inference profile ARNs — `'arn:aws:bedrock:...'`.
151
+ *
152
+ * Used for the Bedrock escape hatch — strings passing this check are
153
+ * accepted into `LlmRoute` even if not in `MODELS.bedrock[]`.
154
+ */
155
+ export declare function isValidBedrockModel(s: string): boolean;
156
+ /**
157
+ * Validate that a (provider, model) pair would construct a valid
158
+ * `LlmRoute`. Two providers have arbitrary-string escape hatches:
159
+ *
160
+ * - `openrouter` — accepts any string passing
161
+ * {@link isValidOpenrouterModel}
162
+ * - `bedrock` — accepts any string passing {@link isValidBedrockModel}
163
+ *
164
+ * Every other provider requires the model to be in
165
+ * `MODELS[provider]`.
166
+ */
167
+ export declare function isValidLlmRoute(provider: string, model: string): boolean;
168
+ /**
169
+ * Serialize an `LlmRoute` to the canonical `provider:model` string.
170
+ * Round-trip with {@link parseLlmRoute}. Used for human-readable
171
+ * logging, `ggui.json` config values, and CLI `--model` flags.
172
+ *
173
+ * Examples:
174
+ * `{provider: 'anthropic', model: 'claude-haiku-4-5-20251001'}`
175
+ * → `'anthropic:claude-haiku-4-5-20251001'`
176
+ * `{provider: 'bedrock', model: 'us.anthropic.claude-haiku-4-5-20251001-v1:0'}`
177
+ * → `'bedrock:us.anthropic.claude-haiku-4-5-20251001-v1:0'`
178
+ * `{provider: 'openrouter', model: 'anthropic/claude-3-5-sonnet'}`
179
+ * → `'openrouter:anthropic/claude-3-5-sonnet'`
180
+ */
181
+ export declare function serializeLlmRoute(route: LlmRoute): string;
182
+ /**
183
+ * Parse a canonical `provider:model` string into an `LlmRoute`.
184
+ * Returns `null` if the string isn't well-formed or if (provider,
185
+ * model) wouldn't construct a valid route. Permissive on the model
186
+ * side for OpenRouter — accepts any string passing
187
+ * {@link isValidOpenrouterModel} even when not in `MODELS.openrouter[]`.
188
+ *
189
+ * Round-trip with {@link serializeLlmRoute}.
190
+ */
191
+ export declare function parseLlmRoute(serialized: string): LlmRoute | null;
192
+ /**
193
+ * Parse a LiteLLM-format string (`<prefix>/<model>` or
194
+ * `<prefix>/<sub>/<model>` for OpenRouter) into an `LlmRoute`.
195
+ * Returns `null` if the prefix is unknown or the resulting route
196
+ * wouldn't construct.
197
+ *
198
+ * Supports the historical formats ggui has used:
199
+ * - `anthropic/claude-haiku-4-5` → `{anthropic, claude-haiku-4-5-20251001}`
200
+ * - `gemini/gemini-3.5-flash` → `{google, gemini-3.5-flash}`
201
+ * - `openai/gpt-5.5-...` → `{openai, gpt-5.5-...}`
202
+ * - `bedrock/us.anthropic...` → `{bedrock, us.anthropic...}`
203
+ * - `openrouter/anthropic/claude-3-5-sonnet`
204
+ * → `{openrouter, anthropic/claude-3-5-sonnet}`
205
+ *
206
+ * Used at wire boundaries (ggui.json parser, CLI flag, env var) so
207
+ * existing operator configs keep working without a forced migration.
208
+ */
209
+ export declare function parseLiteLlmString(s: string): LlmRoute | null;
210
+ /**
211
+ * Serialize an `LlmRoute` to LiteLLM format for outbound observability
212
+ * (Datadog, PostHog, OpenTelemetry `gen_ai.*` semantic conventions
213
+ * recognize LiteLLM IDs). Inverse of {@link parseLiteLlmString}.
214
+ *
215
+ * `{provider: 'anthropic', model: 'claude-haiku-4-5-20251001'}`
216
+ * → `'anthropic/claude-haiku-4-5'`
217
+ *
218
+ * For models without an explicit LITELLM_TO_WIRE mapping (Gemini,
219
+ * OpenAI, OpenRouter), serialization uses the wire model as-is.
220
+ */
221
+ export declare function toLiteLlmString(route: LlmRoute): string;
222
+ /**
223
+ * Single-entry parser that accepts both canonical (`provider:model`)
224
+ * and LiteLLM (`prefix/model`) formats. Used at wire boundaries that
225
+ * receive unvalidated strings — `ggui.json#generation.model`, CLI
226
+ * `--model` flag, env-var defaults.
227
+ *
228
+ * Tries the canonical form first (no internal `/` ambiguity); if it
229
+ * doesn't parse, falls back to LiteLLM. Returns `null` if neither
230
+ * format produces a valid route.
231
+ */
232
+ export declare function parseAnyLlmRoute(s: string): LlmRoute | null;
233
+ //# sourceMappingURL=llm-route.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"llm-route.d.ts","sourceRoot":"","sources":["../../src/types/llm-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAMH;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,MAAM;;;;;;CA2GT,CAAC;AAMX;;;;GAIG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,OAAO,MAAM,CAAC;AAE9C;;;;GAIG;AACH,MAAM,MAAM,YAAY,CAAC,CAAC,SAAS,WAAW,IAAI,CAAC,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;AAE7E;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,MAAM,OAAO,CAAC,CAAC,SAAS,WAAW,IAAI,CAAC,SAC1C,YAAY,GACZ,SAAS,GACT,YAAY,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,GAC/B,YAAY,CAAC,CAAC,CAAC,CAAC;AAEpB;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,QAAQ,GAAG;KACpB,CAAC,IAAI,WAAW,GAAG;QAAE,QAAQ,EAAE,CAAC,CAAC;QAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,CAAA;KAAE;CACvD,CAAC,WAAW,CAAC,CAAC;AAMf;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,GAAG,CAAC,IAAI,WAAW,CAEzD;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,WAAW,EAChD,QAAQ,EAAE,CAAC,EACX,KAAK,EAAE,MAAM,GACZ,KAAK,IAAI,YAAY,CAAC,CAAC,CAAC,CAE1B;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAKzD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAMtD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CASxE;AAiBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,CAEzD;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAQjE;AA8CD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAgB7D;AAuBD;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,CAKvD;AAmBD;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAE3D"}
@@ -0,0 +1,433 @@
1
+ /**
2
+ * Typed `LlmRoute` system — the structural-correctness replacement for
3
+ * string-typed model identifiers.
4
+ *
5
+ * Two concepts, one type:
6
+ * - `provider` — the API endpoint you authenticate against, owns its
7
+ * own model namespace. Includes marketplaces (Bedrock, OpenRouter)
8
+ * and direct-author APIs (Claude/Anthropic, OpenAI, Google AI
9
+ * Studio). NOT a separate "platform" dimension — each provider's
10
+ * API surface IS the platform.
11
+ * - `model` — the wire-canonical string for THIS provider. Registry
12
+ * KEY == what goes on the HTTP wire to the provider. No
13
+ * transformation at dispatch — what you write in `MODELS[provider]`
14
+ * is exactly what the API sees.
15
+ *
16
+ * Why this exists: three bugs of the same class in one week (#22 CLI
17
+ * sent slash-prefixed `google/gemini-3.5-flash` → Gemini 404; #42
18
+ * mcp-server negotiator sent `anthropic/claude-haiku-4-5` → Anthropic
19
+ * 404; the next one would have been...). The pattern was always
20
+ * "someone wrote a code path that bypassed `getUpstreamModelId`". The
21
+ * typed-route system makes the bug class structurally impossible —
22
+ * `LlmRoute` is a discriminated union, the dispatch is
23
+ * exhaustiveness-checked, and there's no transformation step to
24
+ * forget because the model string IS the wire form.
25
+ *
26
+ * Slice spec: `docs/plans/2026-05-25-llm-route-typed-system.md`
27
+ */
28
+ // ============================================================================
29
+ // MODELS registry — single source of truth
30
+ // ============================================================================
31
+ /**
32
+ * Wire-canonical model names per provider. The KEY for each entry is
33
+ * EXACTLY what the provider's API expects on the wire — no
34
+ * transformation, no prefix-strip, no map. Register a new model by
35
+ * adding its wire-canonical string to the matching provider's array.
36
+ *
37
+ * Provider naming uses the COMPANY name for consistency across all
38
+ * providers (`anthropic`, `openai`, `google`) — matches LiteLLM's
39
+ * `anthropic/` prefix + the existing `ANTHROPIC_API_KEY` env var
40
+ * convention. Marketplaces (`bedrock`, `openrouter`) keep their
41
+ * platform name because that IS the company you authenticate against.
42
+ *
43
+ * - `anthropic` — Anthropic's direct API
44
+ * (`api.anthropic.com`). Auth: `ANTHROPIC_API_KEY`.
45
+ * - `openai` — OpenAI's direct API.
46
+ * - `google` — Google AI Studio (`generativelanguage.googleapis.com`).
47
+ * - `bedrock` — AWS Bedrock marketplace. Region prefix (`us.`,
48
+ * `eu.`, `apac.`) is part of the wire name; each region is its
49
+ * own registry entry, not a `{region}` field.
50
+ * - `openrouter` — OpenRouter marketplace. Authors are sub-namespaced
51
+ * in the model string (`<author>/<model>`).
52
+ * - `vertex` — DEFERRED to its own slice. Vertex needs region +
53
+ * projectId + GCP IAM setup; migration is purely additive when
54
+ * ready.
55
+ */
56
+ export const MODELS = {
57
+ anthropic: [
58
+ // Wire-canonical IDs accepted by api.anthropic.com/v1/messages.
59
+ // Per Anthropic's official models doc (claude.com/docs/about-claude/models/overview):
60
+ // 4.6/4.7 generation dropped the date suffix in the wire ID;
61
+ // Haiku 4.5 still uses the dated form.
62
+ 'claude-haiku-4-5-20251001',
63
+ 'claude-sonnet-4-6',
64
+ 'claude-opus-4-7',
65
+ 'claude-opus-4-6',
66
+ ],
67
+ openai: [
68
+ // Per OpenAI's model registry (developers.openai.com/api/docs/models/all).
69
+ // Both unversioned aliases AND dated snapshots are valid wire IDs;
70
+ // we enumerate both because operators reasonably use either.
71
+ 'gpt-5.5',
72
+ 'gpt-5.5-2026-04-23',
73
+ 'gpt-5.5-pro',
74
+ 'gpt-5.5-pro-2026-04-23',
75
+ 'gpt-5.4',
76
+ 'gpt-5.4-2026-03-05',
77
+ 'gpt-5.4-mini',
78
+ 'gpt-5.4-mini-2026-03-17',
79
+ 'gpt-5.4-nano',
80
+ 'gpt-5.4-nano-2026-03-17',
81
+ 'gpt-5.3-codex',
82
+ ],
83
+ google: [
84
+ // Per Google AI Studio's model registry (ai.google.dev/gemini-api/docs/models).
85
+ // Stable + the commonly-used previews. The `-preview` suffix is
86
+ // load-bearing on the wire for preview models — Gemini's API
87
+ // rejects the bare name for those.
88
+ 'gemini-3.5-flash',
89
+ 'gemini-flash-latest',
90
+ 'gemini-3.1-flash-lite',
91
+ 'gemini-3.1-pro-preview',
92
+ 'gemini-3.1-flash-lite-preview',
93
+ 'gemini-3-flash-preview',
94
+ ],
95
+ bedrock: [
96
+ // AWS cross-region inference profile IDs. Each region is its own
97
+ // wire-canonical entry — no `{region}` field on the route.
98
+ // Per AWS docs the 4.6/4.7 generation dropped `-vN:0`; Haiku 4.5
99
+ // keeps it. Coverage per region (us/eu/apac/global) varies per
100
+ // model — verify on individual model card pages under
101
+ // docs.aws.amazon.com/bedrock/latest/userguide/model-cards.html
102
+ // before locking expanded coverage.
103
+ // Haiku 4.5 — has full us/eu/apac/global coverage per Anthropic docs
104
+ 'us.anthropic.claude-haiku-4-5-20251001-v1:0',
105
+ 'eu.anthropic.claude-haiku-4-5-20251001-v1:0',
106
+ 'apac.anthropic.claude-haiku-4-5-20251001-v1:0',
107
+ 'global.anthropic.claude-haiku-4-5-20251001-v1:0',
108
+ // Sonnet 4.6 — current generation, full multi-region per Anthropic docs
109
+ 'us.anthropic.claude-sonnet-4-6',
110
+ 'eu.anthropic.claude-sonnet-4-6',
111
+ 'apac.anthropic.claude-sonnet-4-6',
112
+ // Opus 4.7 — current generation, full multi-region per Anthropic docs
113
+ 'us.anthropic.claude-opus-4-7',
114
+ 'eu.anthropic.claude-opus-4-7',
115
+ 'apac.anthropic.claude-opus-4-7',
116
+ 'global.anthropic.claude-opus-4-7',
117
+ // Opus 4.6 — legacy generation, US-only profile listed in Anthropic docs
118
+ 'us.anthropic.claude-opus-4-6-v1',
119
+ ],
120
+ openrouter: [
121
+ // Commonly-used routes get type-level coverage. Arbitrary
122
+ // `<author>/<model>` strings are also accepted at runtime via the
123
+ // `string & {}` escape hatch on the `ModelOf<'openrouter'>` type
124
+ // — OpenRouter hosts hundreds of model permutations and
125
+ // enumerating all of them in source would create a maintenance
126
+ // burden the type system can't repay.
127
+ // Curated from a live fetch of openrouter.ai/api/v1/models (358
128
+ // models on 2026-05-25). Picks favor the latest stable slug per
129
+ // slot, skipping `:free`, dated `-preview-MMDD`, `-fast`, image/
130
+ // audio/vision-only variants. Note OpenRouter uses dot-separator
131
+ // for Anthropic version (`claude-haiku-4.5`) where Anthropic direct
132
+ // uses dash (`claude-haiku-4-5`) — preserve OpenRouter's wire form.
133
+ // Anthropic family
134
+ 'anthropic/claude-haiku-4.5',
135
+ 'anthropic/claude-sonnet-4.6',
136
+ 'anthropic/claude-opus-4.7',
137
+ // OpenAI family
138
+ 'openai/gpt-5.5',
139
+ 'openai/gpt-5.5-pro',
140
+ 'openai/gpt-5.4-mini',
141
+ 'openai/gpt-5.4-nano',
142
+ // Google family
143
+ 'google/gemini-3.5-flash',
144
+ 'google/gemini-3.1-pro-preview',
145
+ 'google/gemini-3.1-flash-lite',
146
+ // Frontier alternatives
147
+ 'x-ai/grok-4.3',
148
+ // Open-source heavy hitters
149
+ 'meta-llama/llama-4-maverick',
150
+ 'meta-llama/llama-3.3-70b-instruct',
151
+ 'deepseek/deepseek-v4-pro',
152
+ 'deepseek/deepseek-r1-0528',
153
+ 'qwen/qwen3.7-max',
154
+ 'qwen/qwen3-coder',
155
+ 'mistralai/mistral-large-2512',
156
+ 'openai/gpt-oss-120b',
157
+ ],
158
+ };
159
+ // ============================================================================
160
+ // Type guards
161
+ // ============================================================================
162
+ /**
163
+ * Runtime type guard for the provider enum. Pairs with the parser at
164
+ * wire boundaries (ggui.json, CLI flags, env vars) where the value
165
+ * arrives as an unvalidated string.
166
+ */
167
+ export function isLlmProvider(s) {
168
+ return s in MODELS;
169
+ }
170
+ /**
171
+ * Runtime type guard for a known model string under a given provider.
172
+ * Strict-enum check; does NOT accept OpenRouter's arbitrary-string
173
+ * extension — for that, use {@link isValidOpenrouterModel}.
174
+ */
175
+ export function isKnownModel(provider, model) {
176
+ return MODELS[provider].includes(model);
177
+ }
178
+ /**
179
+ * Validate an OpenRouter model string by shape: `<author>/<model>`
180
+ * where both segments are non-empty and contain only the characters
181
+ * OpenRouter's catalog uses (alphanumerics + `-` + `.` + `_` + `:`).
182
+ *
183
+ * Used for the OpenRouter escape hatch — strings that pass this check
184
+ * are accepted into `LlmRoute` even if not in `MODELS.openrouter[]`.
185
+ * Strings that fail it are rejected at the parser boundary.
186
+ */
187
+ export function isValidOpenrouterModel(s) {
188
+ // `<author>/<model>` with both segments non-empty.
189
+ // Permissive char class: alphanumerics, `-`, `.`, `_`, `:`, and one
190
+ // `/` separator. OpenRouter's catalog uses all of these.
191
+ return /^[A-Za-z0-9._:-]+\/[A-Za-z0-9._:-]+$/.test(s);
192
+ }
193
+ /**
194
+ * Validate a Bedrock model string by shape. AWS accepts THREE forms:
195
+ *
196
+ * - Cross-region inference profile ids — `<region>.<inner>` where
197
+ * region is `us`/`eu`/`apac`/`global` and inner contains `.`,
198
+ * `-`, `:`, alphanumerics (e.g.
199
+ * `'us.anthropic.claude-haiku-4-5-20251001-v1:0'`).
200
+ * - Bedrock foundation model ids — bare `<vendor>.<model>` form
201
+ * (e.g. `'anthropic.claude-sonnet-4-6'`).
202
+ * - Inference profile ARNs — `'arn:aws:bedrock:...'`.
203
+ *
204
+ * Used for the Bedrock escape hatch — strings passing this check are
205
+ * accepted into `LlmRoute` even if not in `MODELS.bedrock[]`.
206
+ */
207
+ export function isValidBedrockModel(s) {
208
+ if (s.startsWith('arn:aws:bedrock:'))
209
+ return true;
210
+ // `<segment>.<segment>` with both non-empty + a `.` separator.
211
+ // Permissive char class to cover Bedrock's mix of `.`, `-`, `:`,
212
+ // and alphanumerics.
213
+ return /^[A-Za-z0-9._:-]+\.[A-Za-z0-9._:-]+$/.test(s);
214
+ }
215
+ /**
216
+ * Validate that a (provider, model) pair would construct a valid
217
+ * `LlmRoute`. Two providers have arbitrary-string escape hatches:
218
+ *
219
+ * - `openrouter` — accepts any string passing
220
+ * {@link isValidOpenrouterModel}
221
+ * - `bedrock` — accepts any string passing {@link isValidBedrockModel}
222
+ *
223
+ * Every other provider requires the model to be in
224
+ * `MODELS[provider]`.
225
+ */
226
+ export function isValidLlmRoute(provider, model) {
227
+ if (!isLlmProvider(provider))
228
+ return false;
229
+ if (provider === 'openrouter') {
230
+ return isKnownModel('openrouter', model) || isValidOpenrouterModel(model);
231
+ }
232
+ if (provider === 'bedrock') {
233
+ return isKnownModel('bedrock', model) || isValidBedrockModel(model);
234
+ }
235
+ return isKnownModel(provider, model);
236
+ }
237
+ // ============================================================================
238
+ // Canonical serialization — `provider:model`
239
+ // ============================================================================
240
+ /**
241
+ * Canonical wire format separator. `:` chosen over `/` because
242
+ * `/` collides with OpenRouter's own `<author>/<model>` form
243
+ * (`openrouter/anthropic/claude-3-5-sonnet` has TWO slashes; ambiguous
244
+ * to split). Bedrock's model string also contains `.` and `:` — `:`
245
+ * is fine as the OUTER separator because the inner `:` is always
246
+ * preceded by a `.` or alphanumerics; the FIRST `:` in the serialized
247
+ * string is unambiguously the provider/model boundary.
248
+ */
249
+ const CANONICAL_SEPARATOR = ':';
250
+ /**
251
+ * Serialize an `LlmRoute` to the canonical `provider:model` string.
252
+ * Round-trip with {@link parseLlmRoute}. Used for human-readable
253
+ * logging, `ggui.json` config values, and CLI `--model` flags.
254
+ *
255
+ * Examples:
256
+ * `{provider: 'anthropic', model: 'claude-haiku-4-5-20251001'}`
257
+ * → `'anthropic:claude-haiku-4-5-20251001'`
258
+ * `{provider: 'bedrock', model: 'us.anthropic.claude-haiku-4-5-20251001-v1:0'}`
259
+ * → `'bedrock:us.anthropic.claude-haiku-4-5-20251001-v1:0'`
260
+ * `{provider: 'openrouter', model: 'anthropic/claude-3-5-sonnet'}`
261
+ * → `'openrouter:anthropic/claude-3-5-sonnet'`
262
+ */
263
+ export function serializeLlmRoute(route) {
264
+ return `${route.provider}${CANONICAL_SEPARATOR}${route.model}`;
265
+ }
266
+ /**
267
+ * Parse a canonical `provider:model` string into an `LlmRoute`.
268
+ * Returns `null` if the string isn't well-formed or if (provider,
269
+ * model) wouldn't construct a valid route. Permissive on the model
270
+ * side for OpenRouter — accepts any string passing
271
+ * {@link isValidOpenrouterModel} even when not in `MODELS.openrouter[]`.
272
+ *
273
+ * Round-trip with {@link serializeLlmRoute}.
274
+ */
275
+ export function parseLlmRoute(serialized) {
276
+ const sep = serialized.indexOf(CANONICAL_SEPARATOR);
277
+ if (sep <= 0)
278
+ return null;
279
+ const provider = serialized.substring(0, sep);
280
+ const model = serialized.substring(sep + 1);
281
+ if (model.length === 0)
282
+ return null;
283
+ if (!isValidLlmRoute(provider, model))
284
+ return null;
285
+ return { provider, model };
286
+ }
287
+ // ============================================================================
288
+ // LiteLLM back-compat parser/serializer
289
+ // ============================================================================
290
+ /**
291
+ * Map from LiteLLM transport prefix → our `LlmProvider` enum. Used by
292
+ * {@link parseLiteLlmString} to accept legacy ggui.json files and
293
+ * ecosystem inputs without forcing a migration on every operator.
294
+ *
295
+ * Note: LiteLLM separates `gemini/` (AI Studio) from `vertex_ai/`
296
+ * (enterprise Vertex). We don't have a Vertex variant in this slice;
297
+ * `vertex_ai/` parsing falls through to `null` until Vertex lands.
298
+ */
299
+ const LITELLM_PROVIDER_PREFIX_MAP = {
300
+ anthropic: 'anthropic',
301
+ gemini: 'google',
302
+ openai: 'openai',
303
+ bedrock: 'bedrock',
304
+ openrouter: 'openrouter',
305
+ };
306
+ /**
307
+ * LiteLLM canonical → wire-canonical model mappings for providers
308
+ * whose LiteLLM-form differs from the wire-canonical form.
309
+ * Anthropic's LiteLLM strings (`anthropic/claude-haiku-4-5`) map to
310
+ * dated wire IDs (`claude-haiku-4-5-20251001`).
311
+ *
312
+ * For providers where LiteLLM form == wire form (Gemini, OpenAI,
313
+ * OpenRouter), no mapping is needed — the parser passes the model
314
+ * through after stripping the prefix.
315
+ */
316
+ const LITELLM_TO_WIRE = {
317
+ // Anthropic's 4.6/4.7 generation wire IDs ARE the short LiteLLM
318
+ // form (no date suffix), so no mapping needed for those. Only
319
+ // Haiku 4.5 keeps the dated wire form, so it needs an explicit
320
+ // LiteLLM-short → wire-dated entry. Adding a new mapping here is
321
+ // only required when the LiteLLM short form differs from the wire
322
+ // ID (typically only Anthropic Haiku-class models, which keep
323
+ // dated IDs per Anthropic convention).
324
+ anthropic: {
325
+ 'claude-haiku-4-5': 'claude-haiku-4-5-20251001',
326
+ },
327
+ };
328
+ /**
329
+ * Parse a LiteLLM-format string (`<prefix>/<model>` or
330
+ * `<prefix>/<sub>/<model>` for OpenRouter) into an `LlmRoute`.
331
+ * Returns `null` if the prefix is unknown or the resulting route
332
+ * wouldn't construct.
333
+ *
334
+ * Supports the historical formats ggui has used:
335
+ * - `anthropic/claude-haiku-4-5` → `{anthropic, claude-haiku-4-5-20251001}`
336
+ * - `gemini/gemini-3.5-flash` → `{google, gemini-3.5-flash}`
337
+ * - `openai/gpt-5.5-...` → `{openai, gpt-5.5-...}`
338
+ * - `bedrock/us.anthropic...` → `{bedrock, us.anthropic...}`
339
+ * - `openrouter/anthropic/claude-3-5-sonnet`
340
+ * → `{openrouter, anthropic/claude-3-5-sonnet}`
341
+ *
342
+ * Used at wire boundaries (ggui.json parser, CLI flag, env var) so
343
+ * existing operator configs keep working without a forced migration.
344
+ */
345
+ export function parseLiteLlmString(s) {
346
+ const firstSlash = s.indexOf('/');
347
+ if (firstSlash <= 0)
348
+ return null;
349
+ const prefix = s.substring(0, firstSlash);
350
+ const provider = LITELLM_PROVIDER_PREFIX_MAP[prefix];
351
+ if (!provider)
352
+ return null;
353
+ // For OpenRouter the model is everything after the first slash
354
+ // (which itself includes a `<author>/<model>` sub-path); for
355
+ // every other provider, it's also everything after the first slash
356
+ // but with no internal `/`.
357
+ const rawModel = s.substring(firstSlash + 1);
358
+ if (rawModel.length === 0)
359
+ return null;
360
+ // Apply LiteLLM → wire mapping if one exists for this provider.
361
+ const mapped = LITELLM_TO_WIRE[provider]?.[rawModel] ?? rawModel;
362
+ if (!isValidLlmRoute(provider, mapped))
363
+ return null;
364
+ return { provider, model: mapped };
365
+ }
366
+ /**
367
+ * Inverse mapping for {@link toLiteLlmString} — wire-canonical model
368
+ * back to the LiteLLM short form for a provider's LiteLLM serialization.
369
+ * Built lazily by inverting LITELLM_TO_WIRE on first use.
370
+ */
371
+ let WIRE_TO_LITELLM_CACHE = null;
372
+ function getWireToLitellm() {
373
+ if (WIRE_TO_LITELLM_CACHE)
374
+ return WIRE_TO_LITELLM_CACHE;
375
+ const out = {};
376
+ for (const [provider, map] of Object.entries(LITELLM_TO_WIRE)) {
377
+ if (!map)
378
+ continue;
379
+ const inverted = {};
380
+ for (const [litellm, wire] of Object.entries(map)) {
381
+ inverted[wire] = litellm;
382
+ }
383
+ out[provider] = inverted;
384
+ }
385
+ WIRE_TO_LITELLM_CACHE = out;
386
+ return out;
387
+ }
388
+ /**
389
+ * Serialize an `LlmRoute` to LiteLLM format for outbound observability
390
+ * (Datadog, PostHog, OpenTelemetry `gen_ai.*` semantic conventions
391
+ * recognize LiteLLM IDs). Inverse of {@link parseLiteLlmString}.
392
+ *
393
+ * `{provider: 'anthropic', model: 'claude-haiku-4-5-20251001'}`
394
+ * → `'anthropic/claude-haiku-4-5'`
395
+ *
396
+ * For models without an explicit LITELLM_TO_WIRE mapping (Gemini,
397
+ * OpenAI, OpenRouter), serialization uses the wire model as-is.
398
+ */
399
+ export function toLiteLlmString(route) {
400
+ const prefix = providerLiteLlmPrefix(route.provider);
401
+ const inverseMap = getWireToLitellm()[route.provider];
402
+ const modelForLitellm = inverseMap?.[route.model] ?? route.model;
403
+ return `${prefix}/${modelForLitellm}`;
404
+ }
405
+ function providerLiteLlmPrefix(provider) {
406
+ // Find the LiteLLM prefix that maps to this provider. There's at
407
+ // most one prefix per provider in LITELLM_PROVIDER_PREFIX_MAP today.
408
+ for (const [prefix, p] of Object.entries(LITELLM_PROVIDER_PREFIX_MAP)) {
409
+ if (p === provider)
410
+ return prefix;
411
+ }
412
+ // Defensive: every provider in MODELS should have a LiteLLM prefix.
413
+ // If not, fall through to the provider's own name (degrades gracefully
414
+ // for observability — the trace just won't match LiteLLM's exact
415
+ // wording, but it still names the provider).
416
+ return provider;
417
+ }
418
+ // ============================================================================
419
+ // Combined parser (accepts either format)
420
+ // ============================================================================
421
+ /**
422
+ * Single-entry parser that accepts both canonical (`provider:model`)
423
+ * and LiteLLM (`prefix/model`) formats. Used at wire boundaries that
424
+ * receive unvalidated strings — `ggui.json#generation.model`, CLI
425
+ * `--model` flag, env-var defaults.
426
+ *
427
+ * Tries the canonical form first (no internal `/` ambiguity); if it
428
+ * doesn't parse, falls back to LiteLLM. Returns `null` if neither
429
+ * format produces a valid route.
430
+ */
431
+ export function parseAnyLlmRoute(s) {
432
+ return parseLlmRoute(s) ?? parseLiteLlmString(s);
433
+ }