@route_33/opencode 0.4.5 → 0.4.7-rc.10

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.
@@ -0,0 +1,4266 @@
1
+ // src/v2-entry/create-route33-v2-plugin/create-route33-v2-plugin.ts
2
+ import { Integration as Integration2, Plugin } from "@opencode/plugin";
3
+
4
+ // src/auth/authorization-url-stash/authorization-url-stash.ts
5
+ var FALLBACK_URL = "https://route33.ai";
6
+ var SIGN_IN_INSTRUCTIONS = "Open the Route 33 sign-in link \u2014 your browser will finish at a confirmation page.";
7
+ var FINISHING_INSTRUCTIONS = "Finishing Route 33 setup\u2026";
8
+ var URL_WAIT_BUDGET_MS = 3e4;
9
+ var createAuthorizationUrlStash = () => {
10
+ let resolveUrl;
11
+ const nextUrl = new Promise((resolve2) => {
12
+ resolveUrl = resolve2;
13
+ });
14
+ const browserRunner = (url) => {
15
+ resolveUrl?.(url);
16
+ return Promise.resolve();
17
+ };
18
+ const urlForConnect = async (callback) => {
19
+ let decide;
20
+ const decided = new Promise((resolve2) => {
21
+ decide = resolve2;
22
+ });
23
+ const timer = setTimeout(() => {
24
+ decide({ instructions: SIGN_IN_INSTRUCTIONS, url: FALLBACK_URL });
25
+ }, URL_WAIT_BUDGET_MS);
26
+ void nextUrl.then((url) => {
27
+ clearTimeout(timer);
28
+ decide({ instructions: SIGN_IN_INSTRUCTIONS, url: url.toString() });
29
+ });
30
+ void Promise.resolve(callback).catch(() => void 0).then(() => {
31
+ clearTimeout(timer);
32
+ decide({ instructions: FINISHING_INSTRUCTIONS, url: FALLBACK_URL });
33
+ });
34
+ return decided;
35
+ };
36
+ return { browserRunner, urlForConnect };
37
+ };
38
+
39
+ // src/cli/read-env-base-url/read-env-base-url.ts
40
+ var readEnvBaseUrl = (env = process.env) => {
41
+ const value = env["ROUTE33_BASE_URL"]?.trim();
42
+ return value === "" ? void 0 : value;
43
+ };
44
+
45
+ // src/cli/read-env-api-base/read-env-api-base.ts
46
+ var readEnvApiBase = (env = process.env) => {
47
+ const value = env["ROUTE33_API_BASE"]?.trim();
48
+ return value === "" ? void 0 : value;
49
+ };
50
+
51
+ // src/cli/read-env-auth-issuer/read-env-auth-issuer.ts
52
+ var readEnvAuthIssuer = (env = process.env) => {
53
+ const value = env["ROUTE33_AUTH_ISSUER"]?.trim();
54
+ return value === "" ? void 0 : value;
55
+ };
56
+
57
+ // src/shared/default-auth-issuer-url/default-auth-issuer-url.ts
58
+ var DEFAULT_AUTH_ISSUER_URL = "https://auth.route33.ai";
59
+
60
+ // src/shared/default-base-url/default-base-url.ts
61
+ var DEFAULT_BASE_URL = "https://api.route33.ai/v1";
62
+
63
+ // src/shared/default-management-api-base-url/default-management-api-base-url.ts
64
+ var DEFAULT_MANAGEMENT_API_BASE_URL = "https://mapi.route33.ai/api/v1";
65
+
66
+ // src/shared/default-catalog-ttl-ms/default-catalog-ttl-ms.ts
67
+ var DEFAULT_CATALOG_TTL_MS = 36e5;
68
+
69
+ // src/shared/logger/logger.ts
70
+ var logger = {
71
+ info: (...args) => {
72
+ console.log("[route_33]", ...args);
73
+ },
74
+ warn: (...args) => {
75
+ console.warn("[route_33]", ...args);
76
+ },
77
+ error: (...args) => {
78
+ console.error("[route_33]", ...args);
79
+ }
80
+ };
81
+
82
+ // src/shared/models-dev-ttl-ms/models-dev-ttl-ms.ts
83
+ var MODELS_DEV_TTL_MS = 864e5;
84
+
85
+ // src/shared/native-sdk-api-key-placeholder/native-sdk-api-key-placeholder.ts
86
+ var NATIVE_SDK_API_KEY_PLACEHOLDER = "__route33_fetch_shim_auth__";
87
+
88
+ // src/shared/resolve-cache-dir/resolve-cache-dir.ts
89
+ import * as os from "node:os";
90
+ import * as path from "node:path";
91
+ var resolveCacheDir = (env) => {
92
+ const override = env["ROUTE33_CACHE_DIR"];
93
+ if (override !== void 0 && override !== "") {
94
+ return override;
95
+ }
96
+ return path.join(
97
+ env["XDG_CACHE_HOME"] ?? path.join(os.homedir(), ".cache"),
98
+ "opencode-route33"
99
+ );
100
+ };
101
+
102
+ // src/cli/read-env-cache-dir/read-env-cache-dir.ts
103
+ var readEnvCacheDir = (env = process.env) => resolveCacheDir(env);
104
+
105
+ // src/cli/read-env-catalog-ttl-ms/read-env-catalog-ttl-ms.ts
106
+ var readEnvCatalogTtlMs = (env = process.env) => {
107
+ const value = Number(env["ROUTE33_CATALOG_TTL_MS"]);
108
+ return Number.isFinite(value) && value > 0 ? value : DEFAULT_CATALOG_TTL_MS;
109
+ };
110
+
111
+ // src/cli/read-env-client-id/read-env-client-id.ts
112
+ var readEnvClientId = (env = process.env) => {
113
+ const value = env["ROUTE33_CLIENT_ID"]?.trim();
114
+ return value === "" ? void 0 : value;
115
+ };
116
+
117
+ // src/cli/read-env-direct-api-key/read-env-direct-api-key.ts
118
+ var readEnvDirectApiKey = (env = process.env) => {
119
+ const value = env["ROUTE33_API_KEY"]?.trim();
120
+ return value === "" ? void 0 : value;
121
+ };
122
+
123
+ // src/cli/read-env-disable-id-shim/read-env-disable-id-shim.ts
124
+ var readEnvDisableIdShim = (env = process.env) => env["ROUTE33_DISABLE_ID_SHIM"] === "1";
125
+
126
+ // src/cli/read-env-discovery/read-env-discovery.ts
127
+ var readEnvDiscovery = (env = process.env) => env["ROUTE33_DISCOVERY"] !== "0";
128
+
129
+ // src/cli/read-env-strict/read-env-strict.ts
130
+ var readEnvStrict = (env = process.env) => env["ROUTE33_STRICT"] !== "0";
131
+
132
+ // src/cli/resolve-v1-secret-store-directory/resolve-v1-secret-store-directory.ts
133
+ import * as os2 from "node:os";
134
+ import * as path2 from "node:path";
135
+ var resolveV1SecretStoreDirectory = (options = {}) => {
136
+ const explicit = options.directory?.trim();
137
+ if (explicit !== void 0 && explicit.length > 0) {
138
+ return path2.resolve(explicit);
139
+ }
140
+ const hostConfig = (options.env ?? process.env)["OPENCODE_CONFIG_DIR"]?.trim();
141
+ if (hostConfig !== void 0 && hostConfig.length > 0) {
142
+ return path2.join(path2.resolve(hostConfig), "route33");
143
+ }
144
+ return path2.join(
145
+ options.homeDirectory ?? os2.homedir(),
146
+ ".config",
147
+ "opencode",
148
+ "route33"
149
+ );
150
+ };
151
+
152
+ // src/config/abortable-sleep/abortable-sleep.ts
153
+ var abortableSleep = (milliseconds, signal) => new Promise((resolve2, reject) => {
154
+ if (signal === void 0) {
155
+ setTimeout(resolve2, milliseconds);
156
+ return;
157
+ }
158
+ const abortError = () => {
159
+ const reason = signal.reason;
160
+ if (reason instanceof Error) {
161
+ return reason;
162
+ }
163
+ return new Error(String(reason), { cause: reason });
164
+ };
165
+ if (signal.aborted) {
166
+ reject(abortError());
167
+ return;
168
+ }
169
+ const onAbort = () => {
170
+ clearTimeout(timer);
171
+ reject(abortError());
172
+ };
173
+ const timer = setTimeout(() => {
174
+ signal.removeEventListener("abort", onAbort);
175
+ resolve2();
176
+ }, milliseconds);
177
+ signal.addEventListener("abort", onAbort, { once: true });
178
+ });
179
+
180
+ // src/config/is-resendable-body/is-resendable-body.ts
181
+ var isResendableBody = (body) => body === void 0 || typeof body === "string" || body instanceof ArrayBuffer || ArrayBuffer.isView(body) || body instanceof Blob || body instanceof URLSearchParams || body instanceof FormData;
182
+
183
+ // src/contract/predicates/has-string-fields/has-string-fields.ts
184
+ var hasStringFields = (record, fields) => fields.every((field) => typeof record[field] === "string");
185
+
186
+ // src/contract/predicates/is-nullable-string/is-nullable-string.ts
187
+ var isNullableString = (value) => typeof value === "string" || value === null;
188
+
189
+ // src/contract/predicates/is-owner-type/is-owner-type.ts
190
+ var isOwnerType = (value) => value === "project" || value === "user_personal" || value === "user_project";
191
+
192
+ // src/contract/predicates/is-record/is-record.ts
193
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
194
+
195
+ // src/config/parse-exhaustion-envelope/parse-exhaustion-envelope.ts
196
+ var parseExhaustionEnvelope = async (response) => {
197
+ let body;
198
+ try {
199
+ body = await response.clone().json();
200
+ } catch (error) {
201
+ if (error instanceof Error) {
202
+ return void 0;
203
+ }
204
+ throw error;
205
+ }
206
+ if (!isRecord(body)) {
207
+ return void 0;
208
+ }
209
+ const nestedError = body["error"];
210
+ const nestedCode = isRecord(nestedError) ? nestedError["code"] : void 0;
211
+ if (body["code"] !== "PROVIDER_LANES_EXHAUSTED" && nestedCode !== "PROVIDER_LANES_EXHAUSTED") {
212
+ return void 0;
213
+ }
214
+ const nestedId = isRecord(nestedError) ? nestedError["correlationId"] : void 0;
215
+ const directId = body["correlationId"];
216
+ let correlationId;
217
+ if (typeof directId === "string") {
218
+ correlationId = directId;
219
+ } else if (typeof nestedId === "string") {
220
+ correlationId = nestedId;
221
+ }
222
+ return { correlationId };
223
+ };
224
+
225
+ // src/config/request-model/request-model.ts
226
+ var requestModel = (body) => {
227
+ if (typeof body !== "string") {
228
+ return void 0;
229
+ }
230
+ let parsed;
231
+ try {
232
+ parsed = JSON.parse(body);
233
+ } catch (error) {
234
+ if (error instanceof SyntaxError) {
235
+ return void 0;
236
+ }
237
+ throw error;
238
+ }
239
+ return isRecord(parsed) && typeof parsed["model"] === "string" ? parsed["model"] : void 0;
240
+ };
241
+
242
+ // src/config/retry-wait-seconds/retry-wait-seconds.ts
243
+ var retryWaitSeconds = (options) => {
244
+ const advised = options.retryAfter !== null && /^\d+$/.test(options.retryAfter) ? Number(options.retryAfter) : void 0;
245
+ return advised !== void 0 && advised > 0 ? Math.min(advised, options.maxWaitSeconds) : Math.min(
246
+ options.baseSeconds * 2 ** (options.attempt - 1),
247
+ options.maxWaitSeconds
248
+ );
249
+ };
250
+
251
+ // src/config/exhaustion-retry-fetch/exhaustion-retry-fetch.ts
252
+ var buildExhaustionRetryFetch = (innerFetch, options = {}) => {
253
+ const sleep = options.sleep ?? abortableSleep;
254
+ const maxWaitSeconds = options.maxWaitSeconds ?? 1200;
255
+ const baseSeconds = options.baseSeconds ?? 5;
256
+ const parseRetryableExhaustion = async (response) => {
257
+ if (response.status !== 429) {
258
+ return void 0;
259
+ }
260
+ const contentType = response.headers.get("content-type") ?? "";
261
+ if (!/(?:^|\/)json(?:;|$)|\+json(?:;|$)/i.test(contentType)) {
262
+ return void 0;
263
+ }
264
+ return parseExhaustionEnvelope(response);
265
+ };
266
+ return async (input, init) => {
267
+ const body = init?.body;
268
+ const signal = init?.signal ?? void 0;
269
+ const resendable = isResendableBody(body);
270
+ const model = requestModel(body);
271
+ for (let attempt = 1; ; attempt += 1) {
272
+ const response = await innerFetch(input, init);
273
+ const exhaustion = await parseRetryableExhaustion(response);
274
+ if (exhaustion === void 0 || !resendable) {
275
+ return response;
276
+ }
277
+ const seconds = retryWaitSeconds({
278
+ attempt,
279
+ baseSeconds,
280
+ maxWaitSeconds,
281
+ retryAfter: response.headers.get("retry-after")
282
+ });
283
+ logger.warn("provider lanes exhausted; retrying", {
284
+ attempt,
285
+ correlationId: exhaustion.correlationId,
286
+ ...model === void 0 ? {} : { model },
287
+ seconds
288
+ });
289
+ await sleep(seconds * 1e3, signal);
290
+ signal?.throwIfAborted();
291
+ }
292
+ };
293
+ };
294
+
295
+ // src/config/rewrite-openai-body/rewrite-openai-body.ts
296
+ var rewriteOpenAiBody = (body) => {
297
+ const parseOpenAiRecord = () => {
298
+ let value;
299
+ try {
300
+ value = JSON.parse(body);
301
+ } catch (error) {
302
+ if (error instanceof SyntaxError) {
303
+ return void 0;
304
+ }
305
+ throw error;
306
+ }
307
+ if (!isRecord(value)) {
308
+ return void 0;
309
+ }
310
+ const model = value["model"];
311
+ if (typeof model !== "string" || !model.startsWith("openai/")) {
312
+ return void 0;
313
+ }
314
+ return value;
315
+ };
316
+ const parsed = parseOpenAiRecord();
317
+ if (parsed === void 0) {
318
+ return void 0;
319
+ }
320
+ const tools = parsed["tools"];
321
+ const dropsEffort = "reasoning_effort" in parsed && Array.isArray(tools) && tools.length > 0;
322
+ const renamesMaxTokens = "max_tokens" in parsed;
323
+ if (!dropsEffort && !renamesMaxTokens) {
324
+ return void 0;
325
+ }
326
+ const rewritten = { ...parsed };
327
+ if (dropsEffort) {
328
+ delete rewritten["reasoning_effort"];
329
+ }
330
+ if (renamesMaxTokens) {
331
+ rewritten["max_completion_tokens"] ??= rewritten["max_tokens"];
332
+ delete rewritten["max_tokens"];
333
+ }
334
+ return JSON.stringify(rewritten);
335
+ };
336
+
337
+ // src/config/openai-chat-compat-fetch/openai-chat-compat-fetch.ts
338
+ var buildOpenAiChatCompatFetch = (fetchImpl) => (input, init) => {
339
+ const rewritten = typeof init?.body === "string" ? rewriteOpenAiBody(init.body) : void 0;
340
+ const finalInit = rewritten === void 0 ? init : { ...init, body: rewritten };
341
+ return (fetchImpl ?? globalThis.fetch)(input, finalInit);
342
+ };
343
+
344
+ // src/config/build-options/build-options.ts
345
+ var buildOptions = (baseURL) => ({
346
+ apiKey: NATIVE_SDK_API_KEY_PLACEHOLDER,
347
+ baseURL,
348
+ fetch: buildExhaustionRetryFetch(buildOpenAiChatCompatFetch()),
349
+ headerTimeout: 15e5
350
+ });
351
+
352
+ // src/config/native-sdk-by-vendor/native-sdk-by-vendor.ts
353
+ var NATIVE_SDK_BY_VENDOR = {
354
+ anthropic: "@ai-sdk/anthropic",
355
+ mistralai: "@ai-sdk/mistral",
356
+ openai: "@ai-sdk/openai"
357
+ };
358
+
359
+ // src/config/to-model-patch/to-model-patch.ts
360
+ var toModelPatch = (options) => {
361
+ const entry = { name: options.meta.name };
362
+ if (options.routingSlug !== void 0) {
363
+ entry.id = options.routingSlug;
364
+ }
365
+ if (options.nativeNpm !== void 0) {
366
+ entry.provider = { npm: options.nativeNpm };
367
+ }
368
+ const applyMetadata = () => {
369
+ const capabilities = options.meta.capabilities;
370
+ entry.attachment = options.meta.attachment ?? capabilities.attachment;
371
+ if (options.meta.modalities !== void 0) {
372
+ entry.modalities = structuredClone(options.meta.modalities);
373
+ }
374
+ entry.reasoning = options.forceReasoning ? true : capabilities.reasoning;
375
+ entry.temperature = capabilities.temperature;
376
+ entry.tool_call = capabilities.tool_call;
377
+ if (options.meta.cost !== void 0) {
378
+ entry.cost = structuredClone(options.meta.cost);
379
+ }
380
+ if (options.meta.limit !== void 0) {
381
+ entry.limit = {
382
+ context: options.meta.limit.context,
383
+ output: options.meta.limit.output
384
+ };
385
+ }
386
+ if (options.meta.variants !== void 0) {
387
+ entry.variants = structuredClone(options.meta.variants);
388
+ }
389
+ };
390
+ applyMetadata();
391
+ const compactEntry = { name: entry.name };
392
+ for (const [key, value] of Object.entries(entry)) {
393
+ if (value !== void 0) {
394
+ compactEntry[key] = value;
395
+ }
396
+ }
397
+ return compactEntry;
398
+ };
399
+
400
+ // src/config/build-provider-blocks/build-provider-blocks.ts
401
+ var buildProviderBlocks = (enriched, options) => {
402
+ const models = {};
403
+ for (const [slug, meta] of Object.entries(enriched).sort(
404
+ ([left], [right]) => left.localeCompare(right)
405
+ )) {
406
+ const separator = slug.indexOf("/");
407
+ const vendor = separator === -1 ? slug : slug.slice(0, separator);
408
+ models[slug] = toModelPatch({
409
+ forceReasoning: vendor === "anthropic",
410
+ meta,
411
+ ...NATIVE_SDK_BY_VENDOR[vendor] === void 0 ? {} : { nativeNpm: NATIVE_SDK_BY_VENDOR[vendor] }
412
+ });
413
+ }
414
+ return {
415
+ route_33: {
416
+ models,
417
+ name: "route_33",
418
+ npm: "@ai-sdk/openai-compatible",
419
+ options: buildOptions(options.baseURL)
420
+ }
421
+ };
422
+ };
423
+
424
+ // src/config/collected-ref/collected-ref.ts
425
+ var pushRef = (refs, value, source) => {
426
+ if (typeof value !== "string") {
427
+ return;
428
+ }
429
+ const separator = value.indexOf("/");
430
+ if (separator <= 0 || separator >= value.length - 1) {
431
+ return;
432
+ }
433
+ const providerId = value.slice(0, separator);
434
+ const suffix = value.slice(separator + 1);
435
+ if (providerId === "route_33") {
436
+ refs.push({ providerId, modelKey: suffix, ref: value, source });
437
+ return;
438
+ }
439
+ if (providerId === "route_33_anthropic") {
440
+ refs.push({
441
+ providerId: "route_33",
442
+ modelKey: `anthropic/${suffix}`,
443
+ ref: value,
444
+ source,
445
+ legacyReplacement: `route_33/anthropic/${suffix}`
446
+ });
447
+ }
448
+ };
449
+
450
+ // src/config/collect-cfg-refs/collect-cfg-refs.ts
451
+ var collectCfgRefs = (config) => {
452
+ const refs = [];
453
+ pushRef(refs, config.model, 'opencode config "model"');
454
+ pushRef(refs, config.small_model, 'opencode config "small_model"');
455
+ for (const [name, agent] of Object.entries(config.agent ?? {})) {
456
+ pushRef(refs, agent?.model, `opencode agent "${name}"`);
457
+ }
458
+ return refs;
459
+ };
460
+
461
+ // src/catalog/catalog-fetch-error/catalog-fetch-error.ts
462
+ var CatalogFetchError = class extends Error {
463
+ constructor(reason, message, statusCode) {
464
+ super(`catalog fetch failed (${reason}): ${message}`);
465
+ this.reason = reason;
466
+ this.statusCode = statusCode;
467
+ this.name = "CatalogFetchError";
468
+ }
469
+ reason;
470
+ statusCode;
471
+ };
472
+
473
+ // src/catalog/catalog-cache/cache-file-path/cache-file-path.ts
474
+ import * as path3 from "node:path";
475
+ var cacheFilePath = (keyHash, cacheDir) => path3.join(cacheDir, `catalog-${keyHash}.json`);
476
+
477
+ // src/catalog/catalog-cache/cache-state/cache-state.ts
478
+ var catalogMemory = /* @__PURE__ */ new Map();
479
+
480
+ // src/catalog/catalog-cache/cache-version/cache-version.ts
481
+ var CATALOG_CACHE_VERSION = 2;
482
+
483
+ // src/catalog/catalog-cache/hash-key/hash-key.ts
484
+ import { createHash } from "node:crypto";
485
+ var hashKey = (baseURL, gatewayKey) => createHash("sha256").update(`${baseURL}
486
+ ${gatewayKey.value}`).digest("hex").slice(0, 12);
487
+
488
+ // src/catalog/catalog-cache/is-catalog-model-record/is-catalog-model-record.ts
489
+ var isCatalogModelRecord = (value) => isRecord(value) && Object.values(value).every(
490
+ (model) => isRecord(model) && typeof model["id"] === "string"
491
+ );
492
+
493
+ // src/catalog/catalog-cache/read-disk/read-disk.ts
494
+ import * as fs from "node:fs";
495
+ var readDisk = (keyHash, cacheDir) => {
496
+ try {
497
+ const parsed = JSON.parse(
498
+ fs.readFileSync(cacheFilePath(keyHash, cacheDir), "utf8")
499
+ );
500
+ if (!isRecord(parsed) || parsed["version"] !== CATALOG_CACHE_VERSION || typeof parsed["fetchedAt"] !== "number" || !isCatalogModelRecord(parsed["models"])) {
501
+ return void 0;
502
+ }
503
+ return {
504
+ fetchedAt: parsed["fetchedAt"],
505
+ keyHash,
506
+ models: parsed["models"]
507
+ };
508
+ } catch {
509
+ return void 0;
510
+ }
511
+ };
512
+
513
+ // src/catalog/catalog-cache/read-fresh-catalog/read-fresh-catalog.ts
514
+ var readFreshCatalog = (options) => {
515
+ const keyHash = hashKey(options.baseURL, options.gatewayKey);
516
+ const now = options.now ?? Date.now();
517
+ const requestedTtl = options.catalogTtlMs ?? DEFAULT_CATALOG_TTL_MS;
518
+ const ttl = Number.isFinite(requestedTtl) && requestedTtl > 0 ? requestedTtl : DEFAULT_CATALOG_TTL_MS;
519
+ const inMemory = catalogMemory.get(keyHash);
520
+ if (inMemory !== void 0 && now - inMemory.fetchedAt <= ttl) {
521
+ return inMemory.models;
522
+ }
523
+ const onDisk = readDisk(keyHash, options.cacheDir);
524
+ if (onDisk === void 0 || now - onDisk.fetchedAt > ttl) {
525
+ return void 0;
526
+ }
527
+ catalogMemory.set(keyHash, onDisk);
528
+ return onDisk.models;
529
+ };
530
+
531
+ // src/catalog/catalog-cache/read-stale-catalog/read-stale-catalog.ts
532
+ var readStaleCatalog = (options) => {
533
+ const keyHash = hashKey(options.baseURL, options.gatewayKey);
534
+ return (catalogMemory.get(keyHash) ?? readDisk(keyHash, options.cacheDir))?.models;
535
+ };
536
+
537
+ // src/catalog/catalog-cache/write-catalog/write-catalog.ts
538
+ import * as fs2 from "node:fs";
539
+ var writeCatalog = (options) => {
540
+ const keyHash = hashKey(options.baseURL, options.gatewayKey);
541
+ const record = {
542
+ fetchedAt: options.now ?? Date.now(),
543
+ keyHash,
544
+ models: options.models
545
+ };
546
+ catalogMemory.set(keyHash, record);
547
+ try {
548
+ fs2.mkdirSync(options.cacheDir, { recursive: true });
549
+ fs2.writeFileSync(
550
+ cacheFilePath(keyHash, options.cacheDir),
551
+ JSON.stringify({ version: CATALOG_CACHE_VERSION, ...record })
552
+ );
553
+ } catch (error) {
554
+ logger.warn(
555
+ `could not persist catalog cache: ${error instanceof Error ? error.message : String(error)}`
556
+ );
557
+ }
558
+ };
559
+
560
+ // src/catalog/build-cache-read-options/build-cache-read-options.ts
561
+ var buildCacheReadOptions = (options) => ({
562
+ baseURL: options.baseURL,
563
+ cacheDir: options.cacheDir,
564
+ ...options.catalogTtlMs === void 0 ? {} : { catalogTtlMs: options.catalogTtlMs },
565
+ gatewayKey: options.gatewayKey,
566
+ ...options.now === void 0 ? {} : { now: options.now }
567
+ });
568
+
569
+ // src/catalog/build-fetch-catalog-options/build-fetch-catalog-options.ts
570
+ var buildFetchCatalogOptions = (options) => ({
571
+ baseURL: options.baseURL,
572
+ gatewayKey: options.gatewayKey,
573
+ ...options.disableIdShim === void 0 ? {} : { disableIdShim: options.disableIdShim },
574
+ ...options.fetchImpl === void 0 ? {} : { fetchImpl: options.fetchImpl }
575
+ });
576
+
577
+ // src/catalog/catalog-models-url/catalog-models-url.ts
578
+ var catalogModelsUrl = (baseURL) => {
579
+ let normalizedBaseURL = baseURL;
580
+ while (normalizedBaseURL.endsWith("/")) {
581
+ normalizedBaseURL = normalizedBaseURL.slice(0, -1);
582
+ }
583
+ return `${normalizedBaseURL}/models`;
584
+ };
585
+
586
+ // src/catalog/default-fetch-timeout-ms/default-fetch-timeout-ms.ts
587
+ var DEFAULT_FETCH_TIMEOUT_MS = 5e3;
588
+
589
+ // src/contract/adapters/contract-parse-error/contract-parse-error.ts
590
+ var ContractParseError = class extends Error {
591
+ };
592
+
593
+ // src/contract/adapters/gateway-api-key/gateway-api-key.ts
594
+ var GatewayApiKey = class {
595
+ value;
596
+ constructor(value) {
597
+ this.value = value;
598
+ }
599
+ };
600
+
601
+ // src/contract/adapters/management-jwt/management-jwt.ts
602
+ var ManagementJwt = class {
603
+ value;
604
+ constructor(value) {
605
+ this.value = value;
606
+ }
607
+ };
608
+
609
+ // src/contract/adapters/parse-gateway-key/parse-gateway-key.ts
610
+ var parseGatewayKey = (value) => {
611
+ if (typeof value !== "string" || value.length === 0) {
612
+ throw new Error("gateway key must be non-empty");
613
+ }
614
+ return new GatewayApiKey(value);
615
+ };
616
+
617
+ // src/contract/adapters/parse-management-jwt/parse-management-jwt.ts
618
+ var parseManagementJwt = (value) => {
619
+ if (typeof value !== "string" || value.length === 0) {
620
+ throw new Error("management token must be non-empty");
621
+ }
622
+ return new ManagementJwt(value);
623
+ };
624
+
625
+ // src/contract/adapters/parse-management-subject/parse-management-subject.ts
626
+ var parseManagementSubject = (credential) => {
627
+ const encodedPayload = credential.value.split(".")[1];
628
+ if (encodedPayload === void 0 || encodedPayload.length === 0) {
629
+ throw new ContractParseError("management token carries no usable subject");
630
+ }
631
+ let payload;
632
+ try {
633
+ payload = JSON.parse(
634
+ Buffer.from(encodedPayload, "base64url").toString("utf8")
635
+ );
636
+ } catch {
637
+ throw new ContractParseError("management token payload is not JSON");
638
+ }
639
+ if (!isRecord(payload)) {
640
+ throw new ContractParseError("management token payload is not an object");
641
+ }
642
+ const subject = payload["sub"];
643
+ if (typeof subject !== "string" || subject.length === 0) {
644
+ throw new ContractParseError("management token carries no usable subject");
645
+ }
646
+ return subject;
647
+ };
648
+
649
+ // src/contract/gateway-catalog/parse-catalog-result/parse-catalog-result.ts
650
+ var parseCatalogResult = (value) => {
651
+ if (!isRecord(value) || !Array.isArray(value["data"])) {
652
+ throw new ContractParseError("invalid catalog response");
653
+ }
654
+ return value;
655
+ };
656
+
657
+ // src/catalog/parse-catalog-entries/parse-catalog-entries.ts
658
+ var parseCatalogEntries = (payload) => {
659
+ if (Array.isArray(payload)) {
660
+ return payload;
661
+ }
662
+ try {
663
+ return parseCatalogResult(payload).data;
664
+ } catch (error) {
665
+ if (error instanceof ContractParseError) {
666
+ throw new CatalogFetchError("parse", error.message);
667
+ }
668
+ throw error;
669
+ }
670
+ };
671
+
672
+ // src/catalog/parse-model-capabilities/parse-model-capabilities.ts
673
+ var parseModelCapabilities = (raw) => {
674
+ if (!isRecord(raw)) {
675
+ return void 0;
676
+ }
677
+ const reasoning = raw["reasoning"] === true;
678
+ const toolCall = raw["tool_support"] === true;
679
+ if (!reasoning && !toolCall) {
680
+ return void 0;
681
+ }
682
+ return {
683
+ ...reasoning ? { reasoning: true } : {},
684
+ ...toolCall ? { tool_call: true } : {}
685
+ };
686
+ };
687
+
688
+ // src/catalog/parse-model-limit/parse-model-limit.ts
689
+ var parseModelLimit = (raw) => {
690
+ if (!isRecord(raw)) {
691
+ return void 0;
692
+ }
693
+ const context = raw["context"];
694
+ const output = raw["output"];
695
+ if (typeof context !== "number" || !Number.isInteger(context) || context <= 0 || typeof output !== "number" || !Number.isInteger(output) || output <= 0) {
696
+ return void 0;
697
+ }
698
+ return { context, output };
699
+ };
700
+
701
+ // src/catalog/parse-model-modalities/parse-model-modalities.ts
702
+ var MODEL_MODALITIES = /* @__PURE__ */ new Set([
703
+ "text",
704
+ "audio",
705
+ "image",
706
+ "video",
707
+ "pdf"
708
+ ]);
709
+ var parseModelModalities = (raw) => {
710
+ if (!isRecord(raw) || !Array.isArray(raw["input"]) || !Array.isArray(raw["output"])) {
711
+ return void 0;
712
+ }
713
+ return {
714
+ input: raw["input"].filter(
715
+ (value) => MODEL_MODALITIES.has(value)
716
+ ),
717
+ output: raw["output"].filter(
718
+ (value) => MODEL_MODALITIES.has(value)
719
+ )
720
+ };
721
+ };
722
+
723
+ // src/catalog/parse-model-variants/parse-model-variants.ts
724
+ var parseModelVariants = (raw) => {
725
+ if (!isRecord(raw)) {
726
+ return void 0;
727
+ }
728
+ const variants = {};
729
+ for (const [name, value] of Object.entries(raw)) {
730
+ if (!isRecord(value)) {
731
+ return void 0;
732
+ }
733
+ variants[name] = value;
734
+ }
735
+ return variants;
736
+ };
737
+
738
+ // src/catalog/extract-catalog-model-broadcast/extract-catalog-model-broadcast.ts
739
+ var extractCatalogModelBroadcast = (entry) => {
740
+ const limit = parseModelLimit(entry["limits"]);
741
+ const capabilities = parseModelCapabilities(entry["capabilities"]);
742
+ const variants = parseModelVariants(entry["variants"]);
743
+ const modalities = parseModelModalities(entry["modalities"]);
744
+ return {
745
+ ...limit === void 0 ? {} : { limit },
746
+ ...capabilities === void 0 ? {} : { capabilities },
747
+ ...variants === void 0 ? {} : { variants },
748
+ ...typeof entry["hasZdrLane"] === "boolean" ? { hasZdrLane: entry["hasZdrLane"] } : {},
749
+ ...modalities === void 0 ? {} : { modalities },
750
+ ...typeof entry["attachment"] === "boolean" ? { attachment: entry["attachment"] } : {}
751
+ };
752
+ };
753
+
754
+ // src/catalog/per-million-rate/per-million-rate.ts
755
+ var perMillionRate = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
756
+
757
+ // src/catalog/extract-models-dev-pricing/extract-models-dev-pricing.ts
758
+ var extractModelsDevPricing = (raw) => {
759
+ const input = perMillionRate(raw["input"]);
760
+ const output = perMillionRate(raw["output"]);
761
+ if (input === void 0 || output === void 0) {
762
+ return void 0;
763
+ }
764
+ const rawCacheRead = raw["cache_read"];
765
+ const rawCacheWrite = raw["cache_write"];
766
+ const cacheRead = perMillionRate(rawCacheRead);
767
+ const cacheWrite = perMillionRate(rawCacheWrite);
768
+ if (rawCacheRead !== void 0 && cacheRead === void 0 || rawCacheWrite !== void 0 && cacheWrite === void 0) {
769
+ return void 0;
770
+ }
771
+ return {
772
+ input,
773
+ output,
774
+ currency: "USD",
775
+ ...cacheRead === void 0 ? {} : { cache_read: cacheRead },
776
+ ...cacheWrite === void 0 ? {} : { cache_write: cacheWrite }
777
+ };
778
+ };
779
+
780
+ // src/catalog/per-token-rate/per-token-rate.ts
781
+ var perTokenRate = (value) => {
782
+ if (typeof value !== "string" || value.trim() === "") {
783
+ return void 0;
784
+ }
785
+ const parsed = Number(value);
786
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : void 0;
787
+ };
788
+
789
+ // src/catalog/extract-open-router-pricing/extract-open-router-pricing.ts
790
+ var extractOpenRouterPricing = (raw) => {
791
+ const input = perTokenRate(raw["prompt"]);
792
+ const output = perTokenRate(raw["completion"]);
793
+ if (input === void 0 || output === void 0) {
794
+ return void 0;
795
+ }
796
+ const rawCacheRead = raw["input_cache_read"];
797
+ const rawCacheWrite = raw["input_cache_write"];
798
+ const cacheRead = perTokenRate(rawCacheRead);
799
+ const cacheWrite = perTokenRate(rawCacheWrite);
800
+ if (rawCacheRead !== void 0 && cacheRead === void 0 || rawCacheWrite !== void 0 && cacheWrite === void 0) {
801
+ return void 0;
802
+ }
803
+ return {
804
+ input: Number((input * 1e6).toPrecision(12)),
805
+ output: Number((output * 1e6).toPrecision(12)),
806
+ currency: "USD",
807
+ ...cacheRead === void 0 ? {} : { cache_read: Number((cacheRead * 1e6).toPrecision(12)) },
808
+ ...cacheWrite === void 0 ? {} : { cache_write: Number((cacheWrite * 1e6).toPrecision(12)) }
809
+ };
810
+ };
811
+
812
+ // src/catalog/extract-pricing/extract-pricing.ts
813
+ var OPEN_ROUTER_PRICING_KEYS = [
814
+ "prompt",
815
+ "completion",
816
+ "request",
817
+ "input_cache_read",
818
+ "input_cache_write"
819
+ ];
820
+ var MODELS_DEV_PRICING_KEYS = [
821
+ "input",
822
+ "output",
823
+ "cache_read",
824
+ "cache_write"
825
+ ];
826
+ var extractPricing = (raw) => {
827
+ if (!isRecord(raw) || raw["currency"] !== void 0 && raw["currency"] !== "USD") {
828
+ return void 0;
829
+ }
830
+ const hasOpenRouterKeys = OPEN_ROUTER_PRICING_KEYS.some((key) => key in raw);
831
+ const hasModelsDevKeys = MODELS_DEV_PRICING_KEYS.some((key) => key in raw);
832
+ if (hasOpenRouterKeys === hasModelsDevKeys) {
833
+ return void 0;
834
+ }
835
+ return hasOpenRouterKeys ? extractOpenRouterPricing(raw) : extractModelsDevPricing(raw);
836
+ };
837
+
838
+ // src/catalog/extract-catalog-model-metadata/extract-catalog-model-metadata.ts
839
+ var extractCatalogModelMetadata = (entry) => {
840
+ const displayName = typeof entry["display_name"] === "string" ? entry["display_name"].trim() : "";
841
+ const pricing = isRecord(entry["pricing"]) && extractPricing(entry["pricing"]) !== void 0 ? entry["pricing"] : void 0;
842
+ return {
843
+ ...displayName === "" ? {} : { displayName },
844
+ ...pricing === void 0 ? {} : { pricing }
845
+ };
846
+ };
847
+
848
+ // src/catalog/normalize-model-id/normalize-model-id.ts
849
+ var normalizeId = (raw, disableShim = false) => {
850
+ const id = raw.trim();
851
+ if (id === "") {
852
+ logger.warn("dropping empty model id");
853
+ return void 0;
854
+ }
855
+ if (!id.includes(":")) return id;
856
+ const match = disableShim ? null : /^([^:/]+):global:global:([^:]+):(.+)$/.exec(id);
857
+ if (match?.[2] !== void 0 && match[3] !== void 0)
858
+ return `${match[2]}/${match[3]}`;
859
+ logger.warn(`dropping colon-form model id ${id}`);
860
+ return void 0;
861
+ };
862
+
863
+ // src/catalog/parse-catalog-model/parse-catalog-model.ts
864
+ var parseCatalogModel = (entry, disableIdShim = false) => {
865
+ if (!isRecord(entry) || typeof entry["id"] !== "string") {
866
+ return void 0;
867
+ }
868
+ const id = normalizeId(entry["id"], disableIdShim);
869
+ if (id === void 0) {
870
+ return void 0;
871
+ }
872
+ return {
873
+ id,
874
+ ...extractCatalogModelMetadata(entry),
875
+ ...extractCatalogModelBroadcast(entry)
876
+ };
877
+ };
878
+
879
+ // src/catalog/to-catalog-models/to-catalog-models.ts
880
+ var toCatalogModels = (entries, disableIdShim = false) => {
881
+ const models = {};
882
+ for (const entry of entries) {
883
+ const model = parseCatalogModel(entry, disableIdShim);
884
+ if (model === void 0 || models[model.id] !== void 0) {
885
+ continue;
886
+ }
887
+ models[model.id] = model;
888
+ }
889
+ return models;
890
+ };
891
+
892
+ // src/catalog/fetch-catalog/fetch-catalog.ts
893
+ var fetchCatalog = async (options) => {
894
+ const controller = new AbortController();
895
+ const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
896
+ const timer = setTimeout(() => {
897
+ controller.abort();
898
+ }, timeoutMs);
899
+ const url = catalogModelsUrl(options.baseURL);
900
+ try {
901
+ let response;
902
+ try {
903
+ response = await (options.fetchImpl ?? fetch)(url, {
904
+ headers: { Authorization: `Bearer ${options.gatewayKey.value}` },
905
+ signal: controller.signal
906
+ });
907
+ } catch (error) {
908
+ throw new CatalogFetchError(
909
+ controller.signal.aborted ? "timeout" : "network",
910
+ error instanceof Error ? error.message : String(error)
911
+ );
912
+ }
913
+ if (!response.ok) {
914
+ throw new CatalogFetchError(
915
+ "http",
916
+ `${url} responded ${String(response.status)}`,
917
+ response.status
918
+ );
919
+ }
920
+ let payload;
921
+ try {
922
+ payload = await response.json();
923
+ } catch {
924
+ throw new CatalogFetchError(
925
+ controller.signal.aborted ? "timeout" : "parse",
926
+ controller.signal.aborted ? `body not read within ${String(timeoutMs)}ms` : "response body is not valid JSON"
927
+ );
928
+ }
929
+ const models = toCatalogModels(
930
+ parseCatalogEntries(payload),
931
+ options.disableIdShim
932
+ );
933
+ logger.info(
934
+ `fetched ${String(Object.keys(models).length)} model(s) from ${url}`
935
+ );
936
+ return models;
937
+ } finally {
938
+ clearTimeout(timer);
939
+ }
940
+ };
941
+
942
+ // src/catalog/none-catalog-result/none-catalog-result.ts
943
+ var noneCatalogResult = (reason, intentionalOptOut) => {
944
+ const message = `route33: no model catalog available \u2014 0 models injected (${reason}). route33 is unusable without a catalog: resolve an API key (opencode auth login / ROUTE33_API_KEY) \u2014 the key dictates which models are available.`;
945
+ if (intentionalOptOut) {
946
+ logger.info(message);
947
+ } else {
948
+ logger.warn(message);
949
+ }
950
+ return { models: {}, source: "none" };
951
+ };
952
+
953
+ // src/catalog/catalog-service/catalog-service.ts
954
+ var getCatalog = async (options) => {
955
+ try {
956
+ if (options.discoveryEnabled === false) {
957
+ return noneCatalogResult(
958
+ "discovery disabled via ROUTE33_DISCOVERY=0",
959
+ true
960
+ );
961
+ }
962
+ if (options.gatewayKey === void 0) {
963
+ return noneCatalogResult("no API key resolved", false);
964
+ }
965
+ const cacheOptions = buildCacheReadOptions({
966
+ baseURL: options.baseURL,
967
+ cacheDir: options.cacheDir,
968
+ catalogTtlMs: options.catalogTtlMs,
969
+ gatewayKey: options.gatewayKey,
970
+ now: options.now
971
+ });
972
+ const fresh = readFreshCatalog(cacheOptions);
973
+ if (fresh !== void 0) {
974
+ logger.info(
975
+ `catalog source: cache (${String(Object.keys(fresh).length)} model(s))`
976
+ );
977
+ return { models: fresh, source: "cache" };
978
+ }
979
+ try {
980
+ const live = await fetchCatalog(
981
+ buildFetchCatalogOptions({
982
+ baseURL: options.baseURL,
983
+ disableIdShim: options.disableIdShim,
984
+ fetchImpl: options.fetchImpl,
985
+ gatewayKey: options.gatewayKey
986
+ })
987
+ );
988
+ if (Object.keys(live).length === 0) {
989
+ throw new CatalogFetchError(
990
+ "parse",
991
+ "gateway returned an empty catalog"
992
+ );
993
+ }
994
+ writeCatalog({ ...cacheOptions, models: live });
995
+ logger.info(
996
+ `catalog source: live (${String(Object.keys(live).length)} model(s))`
997
+ );
998
+ return { models: live, source: "live" };
999
+ } catch (error) {
1000
+ logger.warn(
1001
+ `live catalog fetch failed \u2014 trying stale cache: ${error instanceof Error ? error.message : String(error)}`
1002
+ );
1003
+ }
1004
+ const stale = readStaleCatalog(cacheOptions);
1005
+ if (stale !== void 0) {
1006
+ logger.info(
1007
+ `catalog source: stale-cache (${String(Object.keys(stale).length)} model(s))`
1008
+ );
1009
+ return { models: stale, source: "stale-cache" };
1010
+ }
1011
+ return {
1012
+ ...noneCatalogResult("live fetch failed and no cache available", false),
1013
+ liveFetchFailed: true
1014
+ };
1015
+ } catch (error) {
1016
+ logger.error(
1017
+ `unexpected catalog-service failure \u2014 no catalog available: ${error instanceof Error ? error.message : String(error)}`
1018
+ );
1019
+ return noneCatalogResult("unexpected internal failure", false);
1020
+ }
1021
+ };
1022
+
1023
+ // src/enrich/resolve-capabilities/resolve-capabilities.ts
1024
+ var resolveCapabilities = (catalogModel, modelsDevLookup) => {
1025
+ const catalogCapabilities = catalogModel?.capabilities ?? {};
1026
+ const modelsDevCapabilities = modelsDevLookup ?? {};
1027
+ const candidates = [
1028
+ [
1029
+ "reasoning",
1030
+ catalogCapabilities.reasoning ?? modelsDevCapabilities.reasoning
1031
+ ],
1032
+ [
1033
+ "tool_call",
1034
+ catalogCapabilities.tool_call ?? modelsDevCapabilities.tool_call
1035
+ ],
1036
+ ["attachment", modelsDevCapabilities.attachment],
1037
+ ["temperature", modelsDevCapabilities.temperature]
1038
+ ];
1039
+ const result = {};
1040
+ for (const [key, value] of candidates) {
1041
+ if (value !== void 0) {
1042
+ result[key] = value;
1043
+ }
1044
+ }
1045
+ return result;
1046
+ };
1047
+
1048
+ // src/enrich/resolve-model-name/resolve-model-name.ts
1049
+ var resolveModelName = (slug, catalogModel) => catalogModel?.id === slug && catalogModel.displayName !== void 0 ? catalogModel.displayName : slug;
1050
+
1051
+ // src/enrich/build-model-meta/build-model-meta.ts
1052
+ var buildModelMeta = (options) => {
1053
+ const meta = {
1054
+ name: resolveModelName(options.slug, options.catalogModel),
1055
+ capabilities: resolveCapabilities(
1056
+ options.catalogModel,
1057
+ options.modelsDevLookup
1058
+ )
1059
+ };
1060
+ let attachment = options.modelsDevLookup?.attachment;
1061
+ if (options.catalogModel !== void 0) {
1062
+ if (options.catalogModel.modalities !== void 0) {
1063
+ meta.modalities = options.catalogModel.modalities;
1064
+ }
1065
+ if (options.catalogModel.attachment !== void 0) {
1066
+ attachment = options.catalogModel.attachment;
1067
+ }
1068
+ if (options.catalogModel.variants !== void 0) {
1069
+ meta.variants = options.catalogModel.variants;
1070
+ }
1071
+ }
1072
+ if (attachment !== void 0) {
1073
+ meta.attachment = attachment;
1074
+ }
1075
+ if (options.limit !== void 0) {
1076
+ meta.limit = options.limit;
1077
+ }
1078
+ if (options.cost !== void 0) {
1079
+ meta.cost = options.cost;
1080
+ }
1081
+ return meta;
1082
+ };
1083
+
1084
+ // src/enrich/report-metadata-gaps/report-metadata-gaps.ts
1085
+ var reportMetadataGaps = (report) => {
1086
+ if (report.limit === void 0) {
1087
+ if (report.catalogPresent) {
1088
+ logger.warn(
1089
+ `limit metadata unavailable for ${report.slug} \u2014 no context/output ceiling will be enforced until added`
1090
+ );
1091
+ } else {
1092
+ logger.info(
1093
+ `not in the current gateway catalog \u2014 serving cached metadata for ${report.slug}`
1094
+ );
1095
+ }
1096
+ }
1097
+ if (report.cost === void 0) {
1098
+ logger.warn(
1099
+ `cost metadata unavailable for ${report.slug} \u2014 reported cost will be $0 until added`
1100
+ );
1101
+ }
1102
+ };
1103
+
1104
+ // src/enrich/resolve-cost/resolve-cost.ts
1105
+ var resolveCost = (catalogModel, modelsDevLookup) => {
1106
+ const catalogCost = catalogModel?.pricing === void 0 ? void 0 : extractPricing(catalogModel.pricing);
1107
+ return catalogCost ?? modelsDevLookup?.cost;
1108
+ };
1109
+
1110
+ // src/enrich/resolve-limit/resolve-limit.ts
1111
+ var resolveLimit = (catalogModel, modelsDevLookup) => catalogModel?.limit ?? modelsDevLookup?.limit;
1112
+
1113
+ // src/enrich/enrich-model/enrich-model.ts
1114
+ var enrichModel = (slug, sources) => {
1115
+ const modelsDevLookup = sources.modelsDev.lookup(slug);
1116
+ const catalogModel = sources.catalogModel;
1117
+ const catalogPresent = sources.catalogPresent ?? catalogModel !== void 0;
1118
+ const limit = resolveLimit(catalogModel, modelsDevLookup);
1119
+ const cost = resolveCost(catalogModel, modelsDevLookup);
1120
+ reportMetadataGaps({ slug, catalogPresent, limit, cost });
1121
+ return buildModelMeta({ slug, catalogModel, modelsDevLookup, limit, cost });
1122
+ };
1123
+
1124
+ // src/enrich/models-dev/load-models-dev/load-models-dev.ts
1125
+ import * as path5 from "node:path";
1126
+
1127
+ // src/enrich/models-dev/is-expressible-context-tier/is-expressible-context-tier.ts
1128
+ var EXPRESSIBLE_TIER_SIZE = 2e5;
1129
+ var isExpressibleContextTier = (tier) => {
1130
+ const marker = tier["tier"];
1131
+ return isRecord(marker) && marker["type"] === "context" && marker["size"] === EXPRESSIBLE_TIER_SIZE;
1132
+ };
1133
+
1134
+ // src/enrich/models-dev/num/num.ts
1135
+ var num = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
1136
+
1137
+ // src/enrich/models-dev/max-rate/max-rate.ts
1138
+ var maxRate = (current, candidate) => {
1139
+ const rate = num(candidate);
1140
+ if (rate === void 0) {
1141
+ return current;
1142
+ }
1143
+ return current === void 0 || rate > current ? rate : current;
1144
+ };
1145
+
1146
+ // src/enrich/models-dev/parse-rate-block/parse-rate-block.ts
1147
+ var parseRateBlock = (value) => {
1148
+ if (!isRecord(value)) {
1149
+ return void 0;
1150
+ }
1151
+ const input = num(value["input"]);
1152
+ const output = num(value["output"]);
1153
+ if (input === void 0 || output === void 0) {
1154
+ return void 0;
1155
+ }
1156
+ const cacheRead = num(value["cache_read"]);
1157
+ const cacheWrite = num(value["cache_write"]);
1158
+ return {
1159
+ input,
1160
+ output,
1161
+ ...cacheRead === void 0 ? {} : { cache_read: cacheRead },
1162
+ ...cacheWrite === void 0 ? {} : { cache_write: cacheWrite }
1163
+ };
1164
+ };
1165
+
1166
+ // src/enrich/models-dev/collapse-cost-tiers/collapse-cost-tiers.ts
1167
+ var collapseCostTiers = (cost, tiers) => {
1168
+ const collapsed = { ...cost };
1169
+ for (const tier of tiers) {
1170
+ if (isExpressibleContextTier(tier)) {
1171
+ if (collapsed.context_over_200k === void 0) {
1172
+ const preciseTier = parseRateBlock(tier);
1173
+ if (preciseTier !== void 0) {
1174
+ collapsed.context_over_200k = preciseTier;
1175
+ }
1176
+ }
1177
+ continue;
1178
+ }
1179
+ collapsed.input = maxRate(collapsed.input, tier["input"]) ?? collapsed.input;
1180
+ collapsed.output = maxRate(collapsed.output, tier["output"]) ?? collapsed.output;
1181
+ const cacheRead = maxRate(collapsed.cache_read, tier["cache_read"]);
1182
+ const cacheWrite = maxRate(collapsed.cache_write, tier["cache_write"]);
1183
+ if (cacheRead !== void 0) {
1184
+ collapsed.cache_read = cacheRead;
1185
+ }
1186
+ if (cacheWrite !== void 0) {
1187
+ collapsed.cache_write = cacheWrite;
1188
+ }
1189
+ }
1190
+ return collapsed;
1191
+ };
1192
+
1193
+ // src/enrich/models-dev/normalize-cost/normalize-cost.ts
1194
+ var normalizeCost = (raw) => {
1195
+ if (!isRecord(raw)) {
1196
+ return void 0;
1197
+ }
1198
+ const baseInput = num(raw["input"]);
1199
+ const baseOutput = num(raw["output"]);
1200
+ if (baseInput === void 0 || baseOutput === void 0) {
1201
+ return void 0;
1202
+ }
1203
+ const cacheRead = num(raw["cache_read"]);
1204
+ const cacheWrite = num(raw["cache_write"]);
1205
+ const contextOver200k = parseRateBlock(raw["context_over_200k"]);
1206
+ const cost = {
1207
+ input: baseInput,
1208
+ output: baseOutput,
1209
+ ...cacheRead === void 0 ? {} : { cache_read: cacheRead },
1210
+ ...cacheWrite === void 0 ? {} : { cache_write: cacheWrite },
1211
+ ...contextOver200k === void 0 ? {} : { context_over_200k: contextOver200k }
1212
+ };
1213
+ const tiers = Array.isArray(raw["tiers"]) ? raw["tiers"].filter(isRecord) : [];
1214
+ return collapseCostTiers(cost, tiers);
1215
+ };
1216
+
1217
+ // src/enrich/models-dev/normalize-model-limit/normalize-model-limit.ts
1218
+ var normalizeModelLimit = (raw) => {
1219
+ if (!isRecord(raw)) {
1220
+ return void 0;
1221
+ }
1222
+ const context = num(raw["context"]);
1223
+ const output = num(raw["output"]);
1224
+ if (context === void 0 || output === void 0) {
1225
+ return void 0;
1226
+ }
1227
+ const input = num(raw["input"]);
1228
+ return {
1229
+ context,
1230
+ output,
1231
+ ...input === void 0 ? {} : { input }
1232
+ };
1233
+ };
1234
+
1235
+ // src/enrich/models-dev/build-models-dev-lookup/build-models-dev-lookup.ts
1236
+ var buildModelsDevLookup = (model) => {
1237
+ const cost = normalizeCost(model["cost"]);
1238
+ const limit = normalizeModelLimit(model["limit"]);
1239
+ const reasoning = model["reasoning"];
1240
+ const toolCall = model["tool_call"];
1241
+ const attachment = model["attachment"];
1242
+ const temperature = model["temperature"];
1243
+ return {
1244
+ ...cost === void 0 ? {} : { cost },
1245
+ ...limit === void 0 ? {} : { limit },
1246
+ ...typeof reasoning === "boolean" ? { reasoning } : {},
1247
+ ...typeof toolCall === "boolean" ? { tool_call: toolCall } : {},
1248
+ ...typeof attachment === "boolean" ? { attachment } : {},
1249
+ ...typeof temperature === "boolean" ? { temperature } : {}
1250
+ };
1251
+ };
1252
+
1253
+ // src/enrich/models-dev/swap-last/swap-last.ts
1254
+ var swapLast = (value, from, to) => {
1255
+ const index = value.lastIndexOf(from);
1256
+ return index === -1 ? value : `${value.slice(0, index)}${to}${value.slice(index + from.length)}`;
1257
+ };
1258
+
1259
+ // src/enrich/models-dev/vendor-aliases/vendor-aliases.ts
1260
+ var VENDOR_ALIASES = {
1261
+ "z-ai": "zai"
1262
+ };
1263
+
1264
+ // src/enrich/models-dev/probe-candidates/probe-candidates.ts
1265
+ var probeCandidates = (slug) => {
1266
+ const separatorIndex = slug.indexOf("/");
1267
+ if (separatorIndex <= 0 || separatorIndex >= slug.length - 1) {
1268
+ return [];
1269
+ }
1270
+ const vendor = slug.slice(0, separatorIndex);
1271
+ const model = slug.slice(separatorIndex + 1);
1272
+ const alias = VENDOR_ALIASES[vendor];
1273
+ const vendors = alias === void 0 || alias === vendor ? [vendor] : [vendor, alias];
1274
+ const swaps = [];
1275
+ for (const candidate of [
1276
+ model.replaceAll(".", "-"),
1277
+ model.replaceAll("-", "."),
1278
+ swapLast(model, ".", "-"),
1279
+ swapLast(model, "-", ".")
1280
+ ]) {
1281
+ if (candidate !== model && !swaps.includes(candidate)) {
1282
+ swaps.push(candidate);
1283
+ }
1284
+ }
1285
+ return [
1286
+ ...vendors.map((provider) => [provider, model]),
1287
+ ...vendors.flatMap(
1288
+ (provider) => swaps.map((candidate) => [provider, candidate])
1289
+ )
1290
+ ];
1291
+ };
1292
+
1293
+ // src/enrich/models-dev/lookup/lookup.ts
1294
+ var lookupSlug = (data, slug) => {
1295
+ for (const [providerKey, modelKey] of probeCandidates(slug)) {
1296
+ const provider = data[providerKey];
1297
+ if (!isRecord(provider) || !isRecord(provider["models"])) {
1298
+ continue;
1299
+ }
1300
+ const model = provider["models"][modelKey];
1301
+ if (isRecord(model)) {
1302
+ return buildModelsDevLookup(model);
1303
+ }
1304
+ }
1305
+ return void 0;
1306
+ };
1307
+
1308
+ // src/enrich/models-dev/create-models-dev-source/create-models-dev-source.ts
1309
+ var createModelsDevSource = (source, data) => ({
1310
+ source,
1311
+ lookup: (slug) => lookupSlug(data, slug)
1312
+ });
1313
+
1314
+ // src/enrich/models-dev/fetch-live/fetch-live.ts
1315
+ var MODELS_DEV_API_URL = "https://models.dev/api.json";
1316
+ var FETCH_TIMEOUT_MS = 5e3;
1317
+ var fetchLive = async (fetchImpl) => {
1318
+ const controller = new AbortController();
1319
+ const timer = setTimeout(() => {
1320
+ controller.abort();
1321
+ }, FETCH_TIMEOUT_MS);
1322
+ try {
1323
+ const response = await fetchImpl(MODELS_DEV_API_URL, {
1324
+ signal: controller.signal
1325
+ });
1326
+ if (!response.ok) {
1327
+ logger.warn(`models.dev responded ${String(response.status)}`);
1328
+ return void 0;
1329
+ }
1330
+ const body = await response.json();
1331
+ return isRecord(body) ? body : void 0;
1332
+ } catch (error) {
1333
+ logger.warn(
1334
+ "models.dev fetch failed:",
1335
+ error instanceof Error ? error.message : String(error)
1336
+ );
1337
+ return void 0;
1338
+ } finally {
1339
+ clearTimeout(timer);
1340
+ }
1341
+ };
1342
+
1343
+ // src/enrich/models-dev/read-models-dev-cache/read-models-dev-cache.ts
1344
+ import * as fs3 from "node:fs";
1345
+ var readModelsDevCache = (cachePath) => {
1346
+ try {
1347
+ const parsed = JSON.parse(fs3.readFileSync(cachePath, "utf8"));
1348
+ if (!isRecord(parsed)) {
1349
+ return void 0;
1350
+ }
1351
+ const fetchedAt = num(parsed["fetchedAt"]);
1352
+ const data = parsed["data"];
1353
+ if (fetchedAt === void 0 || !isRecord(data)) {
1354
+ return void 0;
1355
+ }
1356
+ return { fetchedAt, data };
1357
+ } catch {
1358
+ return void 0;
1359
+ }
1360
+ };
1361
+
1362
+ // src/enrich/models-dev/write-models-dev-cache/write-models-dev-cache.ts
1363
+ import * as fs4 from "node:fs";
1364
+ import * as path4 from "node:path";
1365
+ var writeModelsDevCache = (cachePath, envelope) => {
1366
+ try {
1367
+ fs4.mkdirSync(path4.dirname(cachePath), { recursive: true });
1368
+ fs4.writeFileSync(cachePath, JSON.stringify(envelope), "utf8");
1369
+ } catch (error) {
1370
+ logger.warn(
1371
+ "failed to write models.dev cache:",
1372
+ error instanceof Error ? error.message : String(error)
1373
+ );
1374
+ }
1375
+ };
1376
+
1377
+ // src/enrich/models-dev/load-models-dev/load-models-dev.ts
1378
+ var CACHE_FILE = "models-dev.json";
1379
+ var loadModelsDev = async (options = {}) => {
1380
+ const fetchImpl = options.fetchImpl ?? fetch;
1381
+ const cacheDir = options.cacheDir ?? resolveCacheDir({});
1382
+ const ttlMs = options.ttlMs ?? MODELS_DEV_TTL_MS;
1383
+ const now = options.now ?? Date.now;
1384
+ const cachePath = path5.join(cacheDir, CACHE_FILE);
1385
+ const cached = readModelsDevCache(cachePath);
1386
+ if (cached !== void 0 && now() - cached.fetchedAt < ttlMs) {
1387
+ return createModelsDevSource("cache", cached.data);
1388
+ }
1389
+ const live = await fetchLive(fetchImpl);
1390
+ if (live !== void 0) {
1391
+ writeModelsDevCache(cachePath, { fetchedAt: now(), data: live });
1392
+ return createModelsDevSource("live", live);
1393
+ }
1394
+ if (cached !== void 0) {
1395
+ logger.warn("models.dev unreachable \u2014 serving stale cache");
1396
+ return createModelsDevSource("stale-cache", cached.data);
1397
+ }
1398
+ logger.warn(
1399
+ "models.dev unavailable (no network, no cache) \u2014 enrichment source disabled"
1400
+ );
1401
+ return { source: null, lookup: () => void 0 };
1402
+ };
1403
+
1404
+ // src/config/enrich-catalog/enrich-catalog.ts
1405
+ var enrichCatalog = (catalog, modelsDev) => {
1406
+ const enriched = {};
1407
+ for (const catalogModel of Object.values(catalog.models)) {
1408
+ enriched[catalogModel.id] = enrichModel(catalogModel.id, {
1409
+ catalogModel,
1410
+ catalogPresent: true,
1411
+ modelsDev
1412
+ });
1413
+ }
1414
+ return enriched;
1415
+ };
1416
+
1417
+ // src/config/merge-provider/merge-provider.ts
1418
+ var mergeProvider = (target, patch) => {
1419
+ target.npm ??= patch.npm;
1420
+ target.name ??= patch.name;
1421
+ if (target.options === void 0) {
1422
+ target.options = { ...patch.options };
1423
+ return;
1424
+ }
1425
+ for (const [field, value] of Object.entries(patch.options)) {
1426
+ const forced = field === "baseURL" || field === "headers" || field === "apiKey" && value !== NATIVE_SDK_API_KEY_PLACEHOLDER;
1427
+ if (forced || target.options[field] === void 0) {
1428
+ target.options[field] = value;
1429
+ }
1430
+ }
1431
+ };
1432
+
1433
+ // src/config/merge-config/merge-config.ts
1434
+ var mergeConfig = (config, blocks) => {
1435
+ config.provider ??= {};
1436
+ const target = config.provider.route_33 ?? {};
1437
+ config.provider.route_33 = target;
1438
+ mergeProvider(target, blocks.route_33);
1439
+ };
1440
+
1441
+ // src/config/regenerate-models/regenerate-models.ts
1442
+ var regenerateModels = (input) => {
1443
+ if (input.source === "none" && input.failedLiveFetch === true) {
1444
+ const current = Object.fromEntries(
1445
+ Object.entries(input.currentModels ?? {}).filter(
1446
+ (entry) => entry[1] !== void 0
1447
+ ).map(([key, entry]) => [key, structuredClone(entry)])
1448
+ );
1449
+ return { models: current, prunedKeys: [], skipped: true };
1450
+ }
1451
+ const models = Object.fromEntries(
1452
+ Object.entries(input.freshModels).map(([key, entry]) => [
1453
+ key,
1454
+ structuredClone(entry)
1455
+ ])
1456
+ );
1457
+ const prunedKeys = Object.keys(input.currentModels ?? {}).filter((key) => input.freshModels[key] === void 0).sort((left, right) => left.localeCompare(right));
1458
+ return { models, prunedKeys };
1459
+ };
1460
+
1461
+ // src/config/remove-legacy-manifest/remove-legacy-manifest.ts
1462
+ import * as fs5 from "node:fs";
1463
+ import * as path6 from "node:path";
1464
+ var removeLegacyManifest = (root) => {
1465
+ const file = path6.join(root, ".opencode", "route33-generated.json");
1466
+ try {
1467
+ if (!fs5.existsSync(file)) {
1468
+ return;
1469
+ }
1470
+ fs5.rmSync(file);
1471
+ logger.info(
1472
+ "route_33: removed obsolete route33-generated.json (regeneration is stateless since 0.4.4)"
1473
+ );
1474
+ } catch (error) {
1475
+ logger.info(
1476
+ `route_33: unable to remove legacy route33-generated.json: ${error instanceof Error ? error.message : String(error)}`
1477
+ );
1478
+ }
1479
+ };
1480
+
1481
+ // src/config/resolve-provider-base-url/resolve-provider-base-url.ts
1482
+ var resolveProviderBaseUrl = (config, override) => {
1483
+ if (override !== void 0) {
1484
+ return override;
1485
+ }
1486
+ const configured = config.provider?.route_33?.options?.["baseURL"];
1487
+ return typeof configured === "string" && configured.trim() !== "" && !configured.startsWith("{env:") ? configured : DEFAULT_BASE_URL;
1488
+ };
1489
+
1490
+ // src/config/configure-route33/configure-route33.ts
1491
+ var configureRoute33 = async (config, options) => {
1492
+ removeLegacyManifest(options.cwd);
1493
+ const gatewayKey = await options.credentialProvider();
1494
+ const baseURL = resolveProviderBaseUrl(config, options.baseURL);
1495
+ const buildCatalogOptions = () => ({
1496
+ baseURL,
1497
+ cacheDir: options.cacheDir,
1498
+ ...options.catalogTtlMs === void 0 ? {} : { catalogTtlMs: options.catalogTtlMs },
1499
+ ...options.disableIdShim === void 0 ? {} : { disableIdShim: options.disableIdShim },
1500
+ ...options.discoveryEnabled === void 0 ? {} : { discoveryEnabled: options.discoveryEnabled },
1501
+ ...options.fetchImpl === void 0 ? {} : { fetchImpl: options.fetchImpl },
1502
+ ...gatewayKey === void 0 ? {} : { gatewayKey }
1503
+ });
1504
+ const buildModelsDevOptions = () => ({
1505
+ cacheDir: options.cacheDir,
1506
+ ...options.fetchImpl === void 0 ? {} : { fetchImpl: options.fetchImpl }
1507
+ });
1508
+ const catalog = await getCatalog(buildCatalogOptions());
1509
+ const modelsDev = await loadModelsDev(buildModelsDevOptions());
1510
+ const blocks = buildProviderBlocks(enrichCatalog(catalog, modelsDev), {
1511
+ baseURL
1512
+ });
1513
+ const mergeableConfig = config;
1514
+ mergeConfig(mergeableConfig, blocks);
1515
+ const provider = mergeableConfig.provider?.route_33;
1516
+ if (provider !== void 0) {
1517
+ const regeneration = regenerateModels({
1518
+ currentModels: provider.models,
1519
+ failedLiveFetch: catalog.liveFetchFailed,
1520
+ freshModels: blocks.route_33.models,
1521
+ source: catalog.source
1522
+ });
1523
+ provider.models = regeneration.models;
1524
+ if (regeneration.skipped === true) {
1525
+ logger.info(
1526
+ "route_33: catalog fetch failed without a cache; preserving existing model entries"
1527
+ );
1528
+ } else {
1529
+ for (const key of regeneration.prunedKeys) {
1530
+ logger.info(
1531
+ `route_33: dropped model entry '${key}' (not in the gateway catalog)`
1532
+ );
1533
+ }
1534
+ }
1535
+ }
1536
+ return {
1537
+ catalogAvailable: catalog.source !== "none",
1538
+ keyResolved: gatewayKey !== void 0
1539
+ };
1540
+ };
1541
+
1542
+ // src/config/gateway-credential-error/gateway-credential-error.ts
1543
+ var GatewayCredentialError = class extends Error {
1544
+ constructor(message, options) {
1545
+ super(message, options);
1546
+ this.name = "GatewayCredentialError";
1547
+ }
1548
+ };
1549
+
1550
+ // src/config/describe-missing/describe-missing.ts
1551
+ var describeMissing = (config, missing) => {
1552
+ const sections = missing.map((entry) => {
1553
+ const available = Object.keys(
1554
+ config.provider?.[entry.providerId]?.models ?? {}
1555
+ ).sort((left, right) => left.localeCompare(right)).map((key) => `${entry.providerId}/${key}`).join(", ");
1556
+ return [
1557
+ ` ${entry.ref}`,
1558
+ ` referenced by: ${entry.sources.join(", ")}`,
1559
+ ...entry.legacyReplacement === void 0 ? [] : [` rename to "${entry.legacyReplacement}"`],
1560
+ ` available on ${entry.providerId}: ${available || "(none)"}`
1561
+ ].join("\n");
1562
+ });
1563
+ return `[route_33] ${String(missing.length)} unresolvable model reference(s):
1564
+
1565
+ ${sections.join("\n\n")}`;
1566
+ };
1567
+
1568
+ // src/config/find-missing-refs/find-missing-refs.ts
1569
+ var findMissingRefs = (config, refs) => {
1570
+ const byRef = /* @__PURE__ */ new Map();
1571
+ for (const ref of refs) {
1572
+ const models = config.provider?.[ref.providerId]?.models;
1573
+ if (ref.legacyReplacement === void 0 && models?.[ref.modelKey] !== void 0) {
1574
+ continue;
1575
+ }
1576
+ const existing = byRef.get(ref.ref);
1577
+ if (existing === void 0) {
1578
+ byRef.set(ref.ref, {
1579
+ providerId: ref.providerId,
1580
+ modelKey: ref.modelKey,
1581
+ ref: ref.ref,
1582
+ sources: [ref.source],
1583
+ ...ref.legacyReplacement === void 0 ? {} : { legacyReplacement: ref.legacyReplacement }
1584
+ });
1585
+ } else if (!existing.sources.includes(ref.source)) {
1586
+ byRef.set(ref.ref, {
1587
+ ...existing,
1588
+ sources: [...existing.sources, ref.source]
1589
+ });
1590
+ }
1591
+ }
1592
+ return [...byRef.values()].sort(
1593
+ (left, right) => left.ref.localeCompare(right.ref)
1594
+ );
1595
+ };
1596
+
1597
+ // src/config/inject-stubs/inject-stubs.ts
1598
+ var injectStubs = (config, missing) => {
1599
+ config.provider ??= {};
1600
+ for (const entry of missing) {
1601
+ const provider = config.provider[entry.providerId] ?? {};
1602
+ config.provider[entry.providerId] = provider;
1603
+ provider.models ??= {};
1604
+ provider.models[entry.modelKey] = { name: entry.modelKey };
1605
+ }
1606
+ };
1607
+
1608
+ // src/config/inject-aliased/inject-aliased.ts
1609
+ var injectAliased = (config, missing, servedKey) => {
1610
+ const models = config.provider?.[missing.providerId]?.models;
1611
+ if (models === void 0) {
1612
+ return;
1613
+ }
1614
+ const served = models[servedKey];
1615
+ if (typeof served !== "object" || served === null || Array.isArray(served)) {
1616
+ return;
1617
+ }
1618
+ models[missing.modelKey] = { ...structuredClone(served), id: servedKey };
1619
+ };
1620
+
1621
+ // src/config/swap-at-digit-boundaries/swap-at-digit-boundaries.ts
1622
+ var swapAtDigitBoundaries = (tail, from, to) => {
1623
+ let result = "";
1624
+ for (let index = 0; index < tail.length; index += 1) {
1625
+ const character = tail.charAt(index);
1626
+ const previous = tail[index - 1];
1627
+ const next = tail[index + 1];
1628
+ const previousIsDigit = previous !== void 0 && previous >= "0" && previous <= "9";
1629
+ const nextIsDigit = next !== void 0 && next >= "0" && next <= "9";
1630
+ result += character === from && previousIsDigit && nextIsDigit ? to : character;
1631
+ }
1632
+ return result;
1633
+ };
1634
+
1635
+ // src/config/resolve-spelling/resolve-spelling.ts
1636
+ var resolveSpelling = (modelKey, availableKeys) => {
1637
+ const separator = modelKey.indexOf("/");
1638
+ const vendor = separator === -1 ? "" : modelKey.slice(0, separator);
1639
+ const tail = separator === -1 ? modelKey : modelKey.slice(separator + 1);
1640
+ const candidates = new Set(
1641
+ [
1642
+ swapAtDigitBoundaries(tail, ".", "-"),
1643
+ swapAtDigitBoundaries(tail, "-", ".")
1644
+ ].filter((candidate) => candidate !== tail)
1645
+ );
1646
+ if (candidates.size === 0) {
1647
+ return void 0;
1648
+ }
1649
+ const matches = availableKeys.filter((key) => {
1650
+ const keySeparator = key.indexOf("/");
1651
+ const keyVendor = keySeparator === -1 ? "" : key.slice(0, keySeparator);
1652
+ const keyTail = keySeparator === -1 ? key : key.slice(keySeparator + 1);
1653
+ return keyVendor === vendor && candidates.has(keyTail);
1654
+ });
1655
+ return matches.length === 1 ? matches[0] : void 0;
1656
+ };
1657
+
1658
+ // src/config/resolve-ref-spellings/resolve-ref-spellings.ts
1659
+ var resolveRefSpellings = (config, missing) => {
1660
+ const unresolved = [];
1661
+ for (const entry of missing) {
1662
+ const servedKey = entry.legacyReplacement === void 0 ? resolveSpelling(
1663
+ entry.modelKey,
1664
+ Object.keys(config.provider?.[entry.providerId]?.models ?? {})
1665
+ ) : void 0;
1666
+ if (servedKey === void 0) {
1667
+ unresolved.push(entry);
1668
+ continue;
1669
+ }
1670
+ injectAliased(config, entry, servedKey);
1671
+ logger.info(
1672
+ `${entry.ref} resolved to the served spelling "${entry.providerId}/${servedKey}" via dot\u2194dash tolerance \u2014 injected an aliased model entry routing to the served id`
1673
+ );
1674
+ }
1675
+ return unresolved;
1676
+ };
1677
+
1678
+ // src/config/validation-error/validation-error.ts
1679
+ var Route33ValidationError = class extends Error {
1680
+ name = "Route33ValidationError";
1681
+ missingRefs;
1682
+ constructor(message, missingRefs) {
1683
+ super(message);
1684
+ this.missingRefs = missingRefs;
1685
+ }
1686
+ };
1687
+
1688
+ // src/config/validation-remediation/validation-remediation.ts
1689
+ var VALIDATION_REMEDIATION = [
1690
+ "Remediation:",
1691
+ " 1. Set ROUTE33_STRICT=0 to downgrade this error to a warning (missing models get a name-only stub entry).",
1692
+ ' 2. Declare the model explicitly under "provider" in opencode.jsonc.',
1693
+ " 3. Verify the authenticated Route 33 account can access this model \u2014 the catalog is credential-scoped."
1694
+ ].join("\n");
1695
+
1696
+ // src/config/validate-refs/validate-refs.ts
1697
+ var validateRefs = (config, options = {}) => {
1698
+ const missing = findMissingRefs(config, collectCfgRefs(config));
1699
+ if (missing.length === 0) {
1700
+ return;
1701
+ }
1702
+ const unresolved = resolveRefSpellings(config, missing);
1703
+ if (unresolved.length === 0) {
1704
+ return;
1705
+ }
1706
+ const details = describeMissing(config, unresolved);
1707
+ const missingIds = unresolved.map((entry) => entry.ref);
1708
+ if (unresolved.some((entry) => entry.legacyReplacement !== void 0)) {
1709
+ throw new Route33ValidationError(details, missingIds);
1710
+ }
1711
+ if (options.strict === false) {
1712
+ logger.warn(
1713
+ `${details}
1714
+
1715
+ (ROUTE33_STRICT=0 \u2014 continuing with name-only stub entries)`
1716
+ );
1717
+ injectStubs(config, unresolved);
1718
+ return;
1719
+ }
1720
+ if (options.keyResolved === false) {
1721
+ logger.warn(
1722
+ `no route_33 gateway credential resolved \u2014 no model catalog available; ${String(unresolved.length)} referenced model(s) remain unavailable: ${missingIds.join(", ")}. Authenticate with Route 33, then restart. (Continuing with name-only stub entries.)`
1723
+ );
1724
+ injectStubs(config, unresolved);
1725
+ return;
1726
+ }
1727
+ if (options.catalogAvailable === false) {
1728
+ logger.warn(
1729
+ `route33 model catalog unavailable \u2014 ${String(unresolved.length)} referenced model(s) could not be validated against the credential-scoped catalog: ${missingIds.join(", ")}. Restore gateway connectivity or declare the model explicitly under "provider" in opencode.jsonc, then restart. (Continuing with name-only stub entries.)`
1730
+ );
1731
+ injectStubs(config, unresolved);
1732
+ return;
1733
+ }
1734
+ throw new Route33ValidationError(
1735
+ `${details}
1736
+
1737
+ ${VALIDATION_REMEDIATION}`,
1738
+ missingIds
1739
+ );
1740
+ };
1741
+
1742
+ // src/config/config-hook/config-hook.ts
1743
+ var createConfigHook = (options) => async (config) => {
1744
+ let state = {
1745
+ catalogAvailable: false,
1746
+ keyResolved: false
1747
+ };
1748
+ try {
1749
+ state = await configureRoute33(config, options);
1750
+ } catch (error) {
1751
+ if (error instanceof GatewayCredentialError) {
1752
+ throw error;
1753
+ }
1754
+ logger.error(
1755
+ `model discovery/injection failed unexpectedly \u2014 opencode startup continues without injected models: ${error instanceof Error ? error.message : String(error)}`
1756
+ );
1757
+ }
1758
+ try {
1759
+ validateRefs(config, {
1760
+ catalogAvailable: state.catalogAvailable,
1761
+ keyResolved: state.keyResolved,
1762
+ strict: options.strict
1763
+ });
1764
+ } catch (error) {
1765
+ if (error instanceof Route33ValidationError) {
1766
+ logger.error(error.message);
1767
+ throw error;
1768
+ }
1769
+ logger.error(
1770
+ `ref validation failed unexpectedly \u2014 skipping: ${error instanceof Error ? error.message : String(error)}`
1771
+ );
1772
+ }
1773
+ };
1774
+
1775
+ // src/key-lease/default-gateway-key-renewal-ratio/default-gateway-key-renewal-ratio.ts
1776
+ var DEFAULT_GATEWAY_KEY_RENEWAL_RATIO = 0.8;
1777
+
1778
+ // src/key-lease/default-gateway-key-ttl-ms/default-gateway-key-ttl-ms.ts
1779
+ var DEFAULT_GATEWAY_KEY_TTL_MS = 15 * 60 * 1e3;
1780
+
1781
+ // src/key-lease/file-secret-store/file-secret-store.ts
1782
+ import { randomUUID } from "node:crypto";
1783
+ import * as fs6 from "node:fs/promises";
1784
+ import * as path7 from "node:path";
1785
+ var FileSecretStore = class {
1786
+ #directory;
1787
+ #file;
1788
+ constructor(options) {
1789
+ if (options.directory.length === 0) {
1790
+ throw new TypeError("File secret store requires a directory");
1791
+ }
1792
+ if (!/^[a-zA-Z0-9._-]+$/u.test(options.scopeId)) {
1793
+ throw new TypeError("File secret store scope is not portable");
1794
+ }
1795
+ this.#directory = options.directory;
1796
+ this.#file = path7.join(options.directory, `lease-${options.scopeId}.json`);
1797
+ }
1798
+ /** Reads and parses the scoped gateway credential.
1799
+ * @returns The persisted credential, when present.
1800
+ */
1801
+ async get() {
1802
+ let contents;
1803
+ try {
1804
+ contents = await fs6.readFile(this.#file, "utf8");
1805
+ } catch (cause) {
1806
+ if (cause instanceof Error && "code" in cause && cause.code === "ENOENT") {
1807
+ return void 0;
1808
+ }
1809
+ throw cause;
1810
+ }
1811
+ const value = JSON.parse(contents);
1812
+ return parseGatewayKey(value);
1813
+ }
1814
+ /** Writes the scoped gateway credential through an atomic private file.
1815
+ * @param key - The credential to persist, or undefined to remove it.
1816
+ * @returns A promise that settles after the file write or removal.
1817
+ */
1818
+ async set(key) {
1819
+ if (key === void 0) {
1820
+ await fs6.rm(this.#file, { force: true });
1821
+ return;
1822
+ }
1823
+ await fs6.mkdir(this.#directory, { mode: 448, recursive: true });
1824
+ await fs6.chmod(this.#directory, 448);
1825
+ const temporary = path7.join(this.#directory, `.lease-${randomUUID()}.tmp`);
1826
+ try {
1827
+ await fs6.writeFile(temporary, `${JSON.stringify(key.value)}
1828
+ `, {
1829
+ encoding: "utf8",
1830
+ flag: "wx",
1831
+ mode: 384
1832
+ });
1833
+ await fs6.rename(temporary, this.#file);
1834
+ } finally {
1835
+ await fs6.rm(temporary, { force: true });
1836
+ }
1837
+ }
1838
+ };
1839
+
1840
+ // src/key-lease/host-storage-secret-store/host-storage-secret-store.ts
1841
+ var HostStorageSecretStore = class {
1842
+ #key;
1843
+ #storage;
1844
+ constructor(options) {
1845
+ this.#key = options.key;
1846
+ this.#storage = options.storage;
1847
+ }
1848
+ /** Reads and parses the scoped gateway credential.
1849
+ * @returns The persisted credential, when present.
1850
+ */
1851
+ async get() {
1852
+ const value = await this.#storage.get(this.#key);
1853
+ return value === void 0 ? void 0 : parseGatewayKey(value);
1854
+ }
1855
+ /** Writes the scoped gateway credential only through host-owned storage.
1856
+ * @param key - The credential to persist, or undefined to remove it.
1857
+ * @returns A promise that settles after the host write or removal.
1858
+ */
1859
+ set(key) {
1860
+ return key === void 0 ? this.#storage.remove(this.#key) : this.#storage.set(this.#key, key.value);
1861
+ }
1862
+ };
1863
+
1864
+ // src/key-lease/key-lease-error/key-lease-error.ts
1865
+ var REDACTED_CAUSE = "Route 33 gateway key lease dependency failed";
1866
+ var KeyLeaseError = class extends Error {
1867
+ constructor(code, options) {
1868
+ super(
1869
+ "Route 33 gateway key lease failed",
1870
+ options?.cause === void 0 ? void 0 : { cause: REDACTED_CAUSE }
1871
+ );
1872
+ this.code = code;
1873
+ this.name = "KeyLeaseError";
1874
+ }
1875
+ code;
1876
+ };
1877
+
1878
+ // src/key-lease/map-renewal-failure/map-renewal-failure.ts
1879
+ var mapRenewalFailure = (options) => {
1880
+ if (options.current === void 0) {
1881
+ throw options.cause;
1882
+ }
1883
+ if (options.now < Date.parse(options.current.expiresAt)) {
1884
+ throw options.cause;
1885
+ }
1886
+ if (!(options.cause instanceof KeyLeaseError)) {
1887
+ throw options.cause;
1888
+ }
1889
+ if (options.cause.code !== "KEY_LEASE_MINT_FAILED") {
1890
+ throw options.cause;
1891
+ }
1892
+ throw new KeyLeaseError("KEY_LEASE_EXPIRED", { cause: options.cause });
1893
+ };
1894
+
1895
+ // src/key-lease/orphan-control/orphan-control.ts
1896
+ var controlOrphans = async (options) => {
1897
+ const now = options.clock();
1898
+ if (!Number.isFinite(now) || !Number.isInteger(options.retentionCycles) || options.retentionCycles <= 0 || !Number.isFinite(options.ttlMs) || options.ttlMs <= 0) {
1899
+ return { kind: "unavailable" };
1900
+ }
1901
+ let keys;
1902
+ try {
1903
+ keys = await options.managementClient.list(options.managementCredential);
1904
+ } catch {
1905
+ return { kind: "unavailable" };
1906
+ }
1907
+ const staleKeys = keys.data.filter(
1908
+ (key) => key.name === options.keyName && key.id !== options.currentKeyId && Date.parse(key.expires_at) <= now
1909
+ );
1910
+ const deletions = await Promise.allSettled(
1911
+ staleKeys.map(
1912
+ (key) => Promise.resolve().then(
1913
+ () => options.managementClient.delete(options.managementCredential, key.id)
1914
+ )
1915
+ )
1916
+ );
1917
+ return {
1918
+ attempted: deletions.length,
1919
+ deleted: deletions.filter(({ status }) => status === "fulfilled").length,
1920
+ kind: "completed"
1921
+ };
1922
+ };
1923
+
1924
+ // src/key-lease/is-management-unauthorized/is-management-unauthorized.ts
1925
+ var isManagementUnauthorized = (cause) => {
1926
+ if (cause instanceof Response) {
1927
+ return cause.status === 401;
1928
+ }
1929
+ if (typeof cause !== "object") {
1930
+ return false;
1931
+ }
1932
+ if (cause === null) {
1933
+ return false;
1934
+ }
1935
+ if ("status" in cause && cause.status === 401) {
1936
+ return true;
1937
+ }
1938
+ if ("statusCode" in cause && cause.statusCode === 401) {
1939
+ return true;
1940
+ }
1941
+ return "response" in cause && cause.response instanceof Response && cause.response.status === 401;
1942
+ };
1943
+
1944
+ // src/key-lease/recover-lease-state/recover-lease-state.ts
1945
+ var recoverLeaseState = async (options) => {
1946
+ let keys;
1947
+ try {
1948
+ keys = (await options.managementClient.list(options.managementCredential)).data;
1949
+ } catch (cause) {
1950
+ throw new KeyLeaseError(
1951
+ isManagementUnauthorized(cause) ? "KEY_LEASE_UNAUTHORIZED" : "KEY_LEASE_MINT_FAILED",
1952
+ { cause }
1953
+ );
1954
+ }
1955
+ const matches = keys.filter((key2) => {
1956
+ if (key2.name !== options.keyName || key2.revoked || key2.status !== "active" || !Number.isFinite(Date.parse(key2.expires_at))) {
1957
+ return false;
1958
+ }
1959
+ switch (options.attribution.kind) {
1960
+ case "personal":
1961
+ return key2.ownership_type === "user_personal" && key2.project_id === null;
1962
+ case "project":
1963
+ return key2.ownership_type === "project" && key2.project_id === options.attribution.projectId;
1964
+ default: {
1965
+ const unexpected = options.attribution;
1966
+ throw new TypeError(`Unexpected attribution: ${String(unexpected)}`);
1967
+ }
1968
+ }
1969
+ });
1970
+ const key = matches.length === 1 ? matches[0] : void 0;
1971
+ if (key === void 0) {
1972
+ return void 0;
1973
+ }
1974
+ const recovered = {
1975
+ attribution: options.attribution,
1976
+ expiresAt: key.expires_at,
1977
+ keyId: key.id
1978
+ };
1979
+ return options.state.compareAndSwap(void 0, recovered) ? recovered : options.state.snapshot();
1980
+ };
1981
+
1982
+ // src/key-lease/reject-lease/reject-lease.ts
1983
+ var rejectLease = async (options) => {
1984
+ const shouldClear = options.storedKey === void 0 || options.storedKey.value === options.rejection.key.value;
1985
+ if (shouldClear) {
1986
+ try {
1987
+ await options.secretStore.set(void 0);
1988
+ } catch (cause) {
1989
+ throw new KeyLeaseError("KEY_LEASE_STORE_UNAVAILABLE", { cause });
1990
+ }
1991
+ if (!options.state.clear(options.current)) {
1992
+ throw new KeyLeaseError("KEY_LEASE_RENEWAL_RACE");
1993
+ }
1994
+ }
1995
+ switch (options.rejection.kind) {
1996
+ case "expired":
1997
+ throw new KeyLeaseError("KEY_LEASE_EXPIRED");
1998
+ case "unauthorized":
1999
+ throw new KeyLeaseError("KEY_LEASE_UNAUTHORIZED");
2000
+ default: {
2001
+ const unexpected = options.rejection;
2002
+ throw new TypeError(`Unexpected lease rejection: ${String(unexpected)}`);
2003
+ }
2004
+ }
2005
+ };
2006
+
2007
+ // src/key-lease/resolve-key-ownership/resolve-key-ownership.ts
2008
+ var resolveKeyOwnership = (options) => {
2009
+ switch (options.attribution.kind) {
2010
+ case "personal":
2011
+ if (options.managementSubject.length === 0) {
2012
+ throw new KeyLeaseError("KEY_LEASE_MINT_FAILED");
2013
+ }
2014
+ return {
2015
+ ownerId: options.managementSubject,
2016
+ ownerType: "user_personal",
2017
+ projectId: null
2018
+ };
2019
+ case "project":
2020
+ return {
2021
+ ownerId: null,
2022
+ ownerType: "project",
2023
+ projectId: options.attribution.projectId
2024
+ };
2025
+ default: {
2026
+ const unexpected = options.attribution;
2027
+ throw new TypeError(`Unexpected attribution: ${String(unexpected)}`);
2028
+ }
2029
+ }
2030
+ };
2031
+
2032
+ // src/key-lease/mint-key/mint-key.ts
2033
+ var mintKey = async (options) => {
2034
+ const now = options.clock();
2035
+ if (!Number.isFinite(now)) {
2036
+ throw new KeyLeaseError("KEY_LEASE_MINT_FAILED");
2037
+ }
2038
+ if (!Number.isFinite(options.installation.ttlMs)) {
2039
+ throw new KeyLeaseError("KEY_LEASE_MINT_FAILED");
2040
+ }
2041
+ if (options.installation.ttlMs <= 0) {
2042
+ throw new KeyLeaseError("KEY_LEASE_MINT_FAILED");
2043
+ }
2044
+ if (options.installation.keyName.length === 0) {
2045
+ throw new KeyLeaseError("KEY_LEASE_MINT_FAILED");
2046
+ }
2047
+ const ownership = resolveKeyOwnership({
2048
+ attribution: options.attribution,
2049
+ managementSubject: options.identity.subject
2050
+ });
2051
+ let material;
2052
+ try {
2053
+ material = await options.managementClient.create(
2054
+ options.identity.credential,
2055
+ {
2056
+ expiresAt: new Date(now + options.installation.ttlMs).toISOString(),
2057
+ name: options.installation.keyName,
2058
+ ...ownership
2059
+ }
2060
+ );
2061
+ } catch (cause) {
2062
+ if (isManagementUnauthorized(cause)) {
2063
+ throw new KeyLeaseError("KEY_LEASE_UNAUTHORIZED", { cause });
2064
+ }
2065
+ throw new KeyLeaseError("KEY_LEASE_MINT_FAILED", { cause });
2066
+ }
2067
+ return {
2068
+ key: material.key,
2069
+ lease: {
2070
+ attribution: options.attribution,
2071
+ expiresAt: material.expires_at,
2072
+ keyId: material.id
2073
+ }
2074
+ };
2075
+ };
2076
+
2077
+ // src/key-lease/renew-key/renew-key.ts
2078
+ var renewals = /* @__PURE__ */ new WeakMap();
2079
+ var renewKey = async (options) => {
2080
+ const inFlight = renewals.get(options.leaseState);
2081
+ if (inFlight !== void 0) {
2082
+ return inFlight;
2083
+ }
2084
+ const expected = options.leaseState.snapshot();
2085
+ const renewal = (async () => {
2086
+ const discard = async (keyId) => {
2087
+ const cleared = options.leaseState.clear(expected);
2088
+ await Promise.allSettled([
2089
+ Promise.resolve().then(() => options.secretStore.set(void 0)),
2090
+ Promise.resolve().then(
2091
+ () => options.mint.managementClient.delete(
2092
+ options.mint.identity.credential,
2093
+ keyId
2094
+ )
2095
+ )
2096
+ ]);
2097
+ return cleared;
2098
+ };
2099
+ let renewed;
2100
+ if (expected === void 0) {
2101
+ renewed = await mintKey(options.mint);
2102
+ } else {
2103
+ try {
2104
+ const material = await options.mint.managementClient.regenerate(
2105
+ options.mint.identity.credential,
2106
+ expected.keyId,
2107
+ new Date(
2108
+ options.mint.clock() + options.mint.installation.ttlMs
2109
+ ).toISOString()
2110
+ );
2111
+ renewed = {
2112
+ key: material.key,
2113
+ lease: {
2114
+ attribution: options.mint.attribution,
2115
+ expiresAt: material.expires_at,
2116
+ keyId: expected.keyId
2117
+ }
2118
+ };
2119
+ } catch (cause) {
2120
+ const cleared = await discard(expected.keyId);
2121
+ if (!cleared) {
2122
+ throw new KeyLeaseError("KEY_LEASE_RENEWAL_RACE", { cause });
2123
+ }
2124
+ throw new KeyLeaseError(
2125
+ isManagementUnauthorized(cause) ? "KEY_LEASE_UNAUTHORIZED" : "KEY_LEASE_MINT_FAILED",
2126
+ { cause }
2127
+ );
2128
+ }
2129
+ }
2130
+ try {
2131
+ await options.secretStore.set(renewed.key);
2132
+ } catch (cause) {
2133
+ const cleared = await discard(renewed.lease.keyId);
2134
+ if (!cleared) {
2135
+ throw new KeyLeaseError("KEY_LEASE_RENEWAL_RACE", { cause });
2136
+ }
2137
+ throw new KeyLeaseError("KEY_LEASE_STORE_UNAVAILABLE", { cause });
2138
+ }
2139
+ if (!options.leaseState.compareAndSwap(expected, renewed.lease)) {
2140
+ await discard(renewed.lease.keyId);
2141
+ throw new KeyLeaseError("KEY_LEASE_RENEWAL_RACE");
2142
+ }
2143
+ return renewed;
2144
+ })();
2145
+ renewals.set(options.leaseState, renewal);
2146
+ try {
2147
+ return await renewal;
2148
+ } finally {
2149
+ if (renewals.get(options.leaseState) === renewal) {
2150
+ renewals.delete(options.leaseState);
2151
+ }
2152
+ }
2153
+ };
2154
+
2155
+ // src/key-lease/resolve-lease-policy/resolve-lease-policy.ts
2156
+ var resolveLeasePolicy = (policy) => {
2157
+ const resolved = {
2158
+ deletePreviousKey: false,
2159
+ renewalRatio: DEFAULT_GATEWAY_KEY_RENEWAL_RATIO,
2160
+ ttlMs: DEFAULT_GATEWAY_KEY_TTL_MS,
2161
+ ...policy
2162
+ };
2163
+ if (!Number.isFinite(resolved.ttlMs)) {
2164
+ throw new KeyLeaseError("KEY_LEASE_MINT_FAILED");
2165
+ }
2166
+ if (resolved.ttlMs <= 0) {
2167
+ throw new KeyLeaseError("KEY_LEASE_MINT_FAILED");
2168
+ }
2169
+ if (!Number.isFinite(resolved.renewalRatio)) {
2170
+ throw new KeyLeaseError("KEY_LEASE_MINT_FAILED");
2171
+ }
2172
+ if (resolved.renewalRatio <= 0) {
2173
+ throw new KeyLeaseError("KEY_LEASE_MINT_FAILED");
2174
+ }
2175
+ if (resolved.renewalRatio >= 1) {
2176
+ throw new KeyLeaseError("KEY_LEASE_MINT_FAILED");
2177
+ }
2178
+ if (resolved.orphanRetentionCycles === void 0) {
2179
+ return resolved;
2180
+ }
2181
+ if (!Number.isInteger(resolved.orphanRetentionCycles)) {
2182
+ throw new KeyLeaseError("KEY_LEASE_MINT_FAILED");
2183
+ }
2184
+ if (resolved.orphanRetentionCycles <= 0) {
2185
+ throw new KeyLeaseError("KEY_LEASE_MINT_FAILED");
2186
+ }
2187
+ return resolved;
2188
+ };
2189
+
2190
+ // src/key-lease/resolve-seam-value/resolve-seam-value.ts
2191
+ var resolveSeamValue = async (value) => typeof value === "function" ? value() : value;
2192
+
2193
+ // src/key-lease/reusable-lease/reusable-lease.ts
2194
+ var reusableLease = (options) => {
2195
+ if (options.current === void 0) {
2196
+ return void 0;
2197
+ }
2198
+ if (options.storedKey === void 0) {
2199
+ return void 0;
2200
+ }
2201
+ if (!options.state.matchesAttribution(options.attribution)) {
2202
+ return void 0;
2203
+ }
2204
+ const renewalAt = Date.parse(options.current.expiresAt) - options.policy.ttlMs * (1 - options.policy.renewalRatio);
2205
+ if (options.now >= renewalAt) {
2206
+ return void 0;
2207
+ }
2208
+ return { key: options.storedKey, lease: options.current };
2209
+ };
2210
+
2211
+ // src/key-lease/lease-for-attribution/lease-for-attribution.ts
2212
+ var leaseForAttribution = async (options) => {
2213
+ const policy = resolveLeasePolicy(options.policy);
2214
+ const clock = options.clock ?? Date.now;
2215
+ const now = clock();
2216
+ if (!Number.isFinite(now)) {
2217
+ throw new KeyLeaseError("KEY_LEASE_MINT_FAILED");
2218
+ }
2219
+ let current = options.installation.state.snapshot();
2220
+ let storedKey;
2221
+ try {
2222
+ storedKey = await options.installation.secretStore.get();
2223
+ } catch (cause) {
2224
+ throw new KeyLeaseError("KEY_LEASE_STORE_UNAVAILABLE", { cause });
2225
+ }
2226
+ if (current === void 0 && storedKey !== void 0) {
2227
+ current = await recoverLeaseState({
2228
+ attribution: options.attribution,
2229
+ keyName: options.installation.keyName,
2230
+ managementClient: options.management.client,
2231
+ managementCredential: await resolveSeamValue(
2232
+ options.management.credential
2233
+ ),
2234
+ state: options.installation.state
2235
+ });
2236
+ }
2237
+ if (options.rejection !== void 0) {
2238
+ return rejectLease({
2239
+ current,
2240
+ rejection: options.rejection,
2241
+ secretStore: options.installation.secretStore,
2242
+ state: options.installation.state,
2243
+ storedKey
2244
+ });
2245
+ }
2246
+ const reusable = reusableLease({
2247
+ attribution: options.attribution,
2248
+ current,
2249
+ now,
2250
+ policy,
2251
+ state: options.installation.state,
2252
+ storedKey
2253
+ });
2254
+ if (reusable !== void 0) {
2255
+ return reusable;
2256
+ }
2257
+ try {
2258
+ const renewed = await renewKey({
2259
+ leaseState: options.installation.state,
2260
+ mint: {
2261
+ attribution: options.attribution,
2262
+ clock,
2263
+ identity: {
2264
+ credential: await resolveSeamValue(options.management.credential),
2265
+ subject: await resolveSeamValue(options.management.subject)
2266
+ },
2267
+ installation: {
2268
+ keyName: options.installation.keyName,
2269
+ ttlMs: policy.ttlMs
2270
+ },
2271
+ managementClient: options.management.client
2272
+ },
2273
+ secretStore: options.installation.secretStore
2274
+ });
2275
+ if (policy.orphanRetentionCycles !== void 0) {
2276
+ await controlOrphans({
2277
+ clock,
2278
+ currentKeyId: renewed.lease.keyId,
2279
+ keyName: options.installation.keyName,
2280
+ managementClient: options.management.client,
2281
+ managementCredential: await resolveSeamValue(
2282
+ options.management.credential
2283
+ ),
2284
+ retentionCycles: policy.orphanRetentionCycles,
2285
+ ttlMs: policy.ttlMs
2286
+ });
2287
+ }
2288
+ return renewed;
2289
+ } catch (cause) {
2290
+ return mapRenewalFailure({ cause, current, now });
2291
+ }
2292
+ };
2293
+
2294
+ // src/attribution/clear-attribution/clear-attribution.ts
2295
+ import * as fs9 from "node:fs";
2296
+
2297
+ // src/attribution/resolve-attribution-file/resolve-attribution-file.ts
2298
+ import * as fs8 from "node:fs";
2299
+ import * as path9 from "node:path";
2300
+
2301
+ // src/attribution/detect-worktree-root/detect-worktree-root.ts
2302
+ import * as fs7 from "node:fs";
2303
+ import * as path8 from "node:path";
2304
+ var detectWorktreeRoot = (startDirectory) => {
2305
+ let directory = fs7.realpathSync(startDirectory);
2306
+ for (; ; ) {
2307
+ const gitEntry = path8.join(directory, ".git");
2308
+ if (fs7.existsSync(gitEntry)) {
2309
+ const entry = fs7.statSync(gitEntry);
2310
+ if (entry.isDirectory() || entry.isFile()) {
2311
+ return directory;
2312
+ }
2313
+ }
2314
+ const parent = path8.dirname(directory);
2315
+ if (parent === directory) {
2316
+ return void 0;
2317
+ }
2318
+ directory = parent;
2319
+ }
2320
+ };
2321
+
2322
+ // src/attribution/resolve-attribution-file/resolve-attribution-file.ts
2323
+ var resolveAttributionFile = (options) => {
2324
+ const worktreeRoot = detectWorktreeRoot(options.directory);
2325
+ if (worktreeRoot !== void 0) {
2326
+ return path9.join(worktreeRoot, ".opencode", "route33.attribution.json");
2327
+ }
2328
+ const fallbackRoots = {
2329
+ "return-undefined": void 0,
2330
+ "use-directory": fs8.realpathSync(options.directory)
2331
+ };
2332
+ const fallbackRoot = fallbackRoots[options.outsideWorktree];
2333
+ return fallbackRoot === void 0 ? void 0 : path9.join(fallbackRoot, ".opencode", "route33.attribution.json");
2334
+ };
2335
+
2336
+ // src/attribution/clear-attribution/clear-attribution.ts
2337
+ var clearAttribution = (options) => {
2338
+ const file = resolveAttributionFile(options);
2339
+ if (file === void 0) {
2340
+ return false;
2341
+ }
2342
+ fs9.rmSync(file, { force: true });
2343
+ return true;
2344
+ };
2345
+
2346
+ // src/attribution/parse-attribution/parse-attribution.ts
2347
+ var parseAttribution = (value) => {
2348
+ if (!isRecord(value)) {
2349
+ throw new ContractParseError("invalid attribution");
2350
+ }
2351
+ if (value["kind"] === "personal") {
2352
+ return { kind: "personal" };
2353
+ }
2354
+ if (value["kind"] === "project" && typeof value["projectId"] === "string") {
2355
+ return { kind: "project", projectId: value["projectId"] };
2356
+ }
2357
+ throw new ContractParseError("invalid attribution");
2358
+ };
2359
+
2360
+ // src/attribution/read-attribution/read-attribution.ts
2361
+ import * as fs10 from "node:fs";
2362
+ var readAttribution = (options) => {
2363
+ const file = resolveAttributionFile(options);
2364
+ if (file === void 0) {
2365
+ return void 0;
2366
+ }
2367
+ try {
2368
+ const parsed = JSON.parse(fs10.readFileSync(file, "utf8"));
2369
+ return parseAttribution(parsed);
2370
+ } catch {
2371
+ return void 0;
2372
+ }
2373
+ };
2374
+
2375
+ // src/attribution/write-attribution/write-attribution.ts
2376
+ import { randomUUID as randomUUID2 } from "node:crypto";
2377
+ import * as fs11 from "node:fs";
2378
+ import * as path10 from "node:path";
2379
+ var writeAttribution = (options, attribution) => {
2380
+ const file = resolveAttributionFile(options);
2381
+ if (file === void 0) {
2382
+ return false;
2383
+ }
2384
+ fs11.mkdirSync(path10.dirname(file), { recursive: true });
2385
+ const temporaryFile = `${file}.${String(process.pid)}.${randomUUID2()}.tmp`;
2386
+ try {
2387
+ fs11.writeFileSync(temporaryFile, `${JSON.stringify(attribution)}
2388
+ `, "utf8");
2389
+ fs11.renameSync(temporaryFile, file);
2390
+ } catch (error) {
2391
+ fs11.rmSync(temporaryFile, { force: true });
2392
+ throw error;
2393
+ }
2394
+ return true;
2395
+ };
2396
+
2397
+ // src/key-lease/lease-state/lease-state.ts
2398
+ var LeaseState = class _LeaseState {
2399
+ /** Current committed snapshot; mutation is limited to compare-and-swap transitions. */
2400
+ #current;
2401
+ constructor(current) {
2402
+ this.#current = current;
2403
+ }
2404
+ /** Restores untrusted persisted metadata or an empty lease.
2405
+ * @param value - Persisted metadata, or undefined for a new installation.
2406
+ * @returns A lease-state coordinator.
2407
+ */
2408
+ static restore(value) {
2409
+ if (value === void 0) {
2410
+ return new _LeaseState(void 0);
2411
+ }
2412
+ if (!isRecord(value)) {
2413
+ throw new ContractParseError("invalid gateway key lease state");
2414
+ }
2415
+ const fields = Object.keys(value);
2416
+ if (fields.length !== 3 || fields.some(
2417
+ (field) => field !== "attribution" && field !== "expiresAt" && field !== "keyId"
2418
+ )) {
2419
+ throw new ContractParseError("invalid gateway key lease state");
2420
+ }
2421
+ const expiresAt = value["expiresAt"];
2422
+ const keyId = value["keyId"];
2423
+ if (typeof expiresAt !== "string" || !Number.isFinite(Date.parse(expiresAt)) || typeof keyId !== "string" || keyId.length === 0) {
2424
+ throw new ContractParseError("invalid gateway key lease state");
2425
+ }
2426
+ return new _LeaseState({
2427
+ attribution: parseAttribution(value["attribution"]),
2428
+ expiresAt,
2429
+ keyId
2430
+ });
2431
+ }
2432
+ /** Reads the current non-secret metadata.
2433
+ * @returns The current lease metadata, if one is committed.
2434
+ */
2435
+ snapshot() {
2436
+ return this.#current;
2437
+ }
2438
+ /** Checks whether current metadata belongs to an attribution.
2439
+ * @param attribution - The requested ownership choice.
2440
+ * @returns Whether the current lease has the same attribution.
2441
+ */
2442
+ matchesAttribution(attribution) {
2443
+ if (this.#current === void 0) {
2444
+ return false;
2445
+ }
2446
+ switch (this.#current.attribution.kind) {
2447
+ case "personal":
2448
+ return attribution.kind === "personal";
2449
+ case "project":
2450
+ return attribution.kind === "project" && attribution.projectId === this.#current.attribution.projectId;
2451
+ default:
2452
+ return _LeaseState.#unexpectedAttribution(this.#current.attribution);
2453
+ }
2454
+ }
2455
+ /** Atomically swaps metadata when the expected snapshot is still current.
2456
+ * @param expected - Snapshot observed before asynchronous renewal.
2457
+ * @param next - Fresh metadata to commit.
2458
+ * @returns Whether the swap committed.
2459
+ */
2460
+ compareAndSwap(expected, next) {
2461
+ if (this.#current !== expected) {
2462
+ return false;
2463
+ }
2464
+ this.#current = next;
2465
+ return true;
2466
+ }
2467
+ /** Clears metadata only while the expected snapshot remains current.
2468
+ * @param expected - Snapshot whose lease is being invalidated.
2469
+ * @returns Whether the state was cleared.
2470
+ */
2471
+ clear(expected) {
2472
+ if (this.#current !== expected) {
2473
+ return false;
2474
+ }
2475
+ this.#current = void 0;
2476
+ return true;
2477
+ }
2478
+ static #unexpectedAttribution(value) {
2479
+ throw new TypeError(`Unexpected attribution: ${String(value)}`);
2480
+ }
2481
+ };
2482
+
2483
+ // src/key-lease/lease-installation-factory/lease-installation-factory.ts
2484
+ var createLeaseInstallationFactory = (options) => {
2485
+ const installations = /* @__PURE__ */ new Map();
2486
+ return (scopeId) => {
2487
+ const existing = installations.get(scopeId);
2488
+ if (existing !== void 0) {
2489
+ return existing;
2490
+ }
2491
+ const installation = {
2492
+ keyName: `route33:lease:${scopeId}`,
2493
+ secretStore: options.createSecretStore(scopeId),
2494
+ ...options.createStateStore === void 0 ? {} : { stateStore: options.createStateStore(scopeId) },
2495
+ state: LeaseState.restore(void 0)
2496
+ };
2497
+ installations.set(scopeId, installation);
2498
+ return installation;
2499
+ };
2500
+ };
2501
+
2502
+ // src/key-lease/scope-id/scope-id.ts
2503
+ import { createHash as createHash2 } from "node:crypto";
2504
+ var deriveScopeId = (input) => {
2505
+ let identity;
2506
+ switch (input.attribution.kind) {
2507
+ case "personal":
2508
+ identity = ["route33-lease", "personal"];
2509
+ break;
2510
+ case "project":
2511
+ if (input.folderRealpath === void 0 || input.folderRealpath.length === 0) {
2512
+ throw new TypeError("Project lease scope requires a real folder path");
2513
+ }
2514
+ identity = [
2515
+ "route33-lease",
2516
+ "project",
2517
+ input.attribution.projectId,
2518
+ input.folderRealpath
2519
+ ];
2520
+ break;
2521
+ default: {
2522
+ const unexpected = input.attribution;
2523
+ throw new TypeError(`Unexpected attribution: ${String(unexpected)}`);
2524
+ }
2525
+ }
2526
+ return createHash2("sha256").update(JSON.stringify(identity)).digest("hex");
2527
+ };
2528
+
2529
+ // src/requested-client-id/requested-client-id.ts
2530
+ var DEFAULT_ROUTE33_CLIENT_ID = false ? "" : "oa_platform_203623de";
2531
+
2532
+ // src/commands/change-attribution/change-attribution.ts
2533
+ var changeAttribution = async (options) => {
2534
+ await options.clear();
2535
+ return options.prompt();
2536
+ };
2537
+
2538
+ // src/commands/inspect-session/inspect-session.ts
2539
+ var inspectSession = (options) => {
2540
+ const attribution = options.readAttribution();
2541
+ const lease = options.readLease();
2542
+ return {
2543
+ attribution,
2544
+ ...lease === void 0 ? {} : { lease: { expiresAt: lease.expiresAt, keyId: lease.keyId } }
2545
+ };
2546
+ };
2547
+
2548
+ // src/commands/reset-attribution/reset-attribution.ts
2549
+ var resetAttribution = async (options) => {
2550
+ await options.clear();
2551
+ };
2552
+
2553
+ // src/commands/rotate-now/rotate-now.ts
2554
+ var rotateNow = async (options) => {
2555
+ const rotated = await options.lease({ renewalRatio: Number.EPSILON });
2556
+ return rotated.lease;
2557
+ };
2558
+
2559
+ // src/ux/attribution-prompt/attribution-prompt.ts
2560
+ var createAttributionPrompt = (projects) => {
2561
+ const choicesBySelection = {
2562
+ personal: { kind: "personal" }
2563
+ };
2564
+ const projectOptions = projects.map((project) => {
2565
+ const selection = `project:${project.id}`;
2566
+ choicesBySelection[selection] = {
2567
+ kind: "project",
2568
+ projectId: project.id
2569
+ };
2570
+ return {
2571
+ label: `${project.name} (${project.organisation.name})`,
2572
+ selection
2573
+ };
2574
+ });
2575
+ return {
2576
+ choicesBySelection,
2577
+ options: [
2578
+ ...projectOptions,
2579
+ { label: "Use my personal key", selection: "personal" }
2580
+ ],
2581
+ title: "Choose where Route 33 should manage this key"
2582
+ };
2583
+ };
2584
+
2585
+ // src/ux/attribution-prompt-error/attribution-prompt-error.ts
2586
+ var MESSAGES = {
2587
+ ATTRIBUTION_PRESENTER_UNAVAILABLE: "Route 33 needs OpenCode v2 to assign a budget for this folder. Update to OpenCode v2, run /connect, choose Route 33, and follow the steps.",
2588
+ ATTRIBUTION_SELECTION_INVALID: "That Route 33 option is no longer available. Connect again and pick another."
2589
+ };
2590
+ var AttributionPromptError = class extends GatewayCredentialError {
2591
+ code;
2592
+ constructor(code) {
2593
+ super(MESSAGES[code]);
2594
+ this.name = "AttributionPromptError";
2595
+ this.code = code;
2596
+ }
2597
+ };
2598
+
2599
+ // src/ux/prompt-flow/prompt-flow.ts
2600
+ var resolveAttribution = async (options) => {
2601
+ const remembered = readAttribution(options.state);
2602
+ if (remembered !== void 0) {
2603
+ return remembered;
2604
+ }
2605
+ if (options.presenter === void 0) {
2606
+ throw new AttributionPromptError("ATTRIBUTION_PRESENTER_UNAVAILABLE");
2607
+ }
2608
+ const plan = createAttributionPrompt(await options.loadProjects());
2609
+ const selection = await options.presenter.pick(plan);
2610
+ const attribution = plan.choicesBySelection[selection];
2611
+ if (attribution === void 0) {
2612
+ throw new AttributionPromptError("ATTRIBUTION_SELECTION_INVALID");
2613
+ }
2614
+ writeAttribution(options.state, attribution);
2615
+ return attribution;
2616
+ };
2617
+
2618
+ // src/ux/trusted-party-copy/trusted-party-copy.ts
2619
+ var TRUSTED_PARTY_COPY = {
2620
+ message: "Route 33 wants to manage your keys for this OpenCode installation.",
2621
+ title: "Connect Route 33"
2622
+ };
2623
+
2624
+ // src/auth/auth-error/auth-error.ts
2625
+ var AuthError = class extends Error {
2626
+ constructor(code, options) {
2627
+ super(
2628
+ "Route 33 authorization failed",
2629
+ options?.cause === void 0 ? void 0 : { cause: options.cause }
2630
+ );
2631
+ this.code = code;
2632
+ this.name = "AuthError";
2633
+ this.statusCode = options?.statusCode;
2634
+ }
2635
+ code;
2636
+ statusCode;
2637
+ };
2638
+
2639
+ // src/auth/build-authorize-url/build-authorize-url.ts
2640
+ var buildAuthorizeUrl = (issuerUrl, request) => {
2641
+ const url = new URL("/authorize", issuerUrl);
2642
+ url.search = new URLSearchParams({
2643
+ client_id: request.clientId,
2644
+ code_challenge: request.codeChallenge,
2645
+ code_challenge_method: "S256",
2646
+ redirect_uri: request.redirectUri,
2647
+ response_type: "code",
2648
+ scope: request.scope,
2649
+ state: request.state
2650
+ }).toString();
2651
+ return url;
2652
+ };
2653
+
2654
+ // src/contract/auth-session/parse-token-set/parse-token-set.ts
2655
+ var parseTokenSet = (value) => {
2656
+ if (!isRecord(value)) {
2657
+ throw new ContractParseError("invalid token response");
2658
+ }
2659
+ if (typeof value["access_token"] !== "string" || typeof value["expires_in"] !== "number" || value["token_type"] !== "Bearer") {
2660
+ throw new ContractParseError("invalid token response");
2661
+ }
2662
+ return {
2663
+ access_token: value["access_token"],
2664
+ expires_in: value["expires_in"],
2665
+ token_type: "Bearer",
2666
+ ...typeof value["id_token"] === "string" ? { id_token: value["id_token"] } : {},
2667
+ ...typeof value["refresh_token"] === "string" ? { refresh_token: value["refresh_token"] } : {},
2668
+ ...typeof value["scope"] === "string" ? { scope: value["scope"] } : {}
2669
+ };
2670
+ };
2671
+
2672
+ // src/auth/exchange-code-and-parse/exchange-code-and-parse.ts
2673
+ var exchangeCodeAndParse = async (options) => {
2674
+ const body = new URLSearchParams({
2675
+ client_id: options.clientId,
2676
+ code: options.input.code,
2677
+ code_verifier: options.input.codeVerifier,
2678
+ grant_type: "authorization_code",
2679
+ redirect_uri: options.input.redirectUri
2680
+ });
2681
+ let response;
2682
+ try {
2683
+ response = await (options.fetchImpl ?? fetch)(options.tokenUrl, {
2684
+ body,
2685
+ headers: { "content-type": "application/x-www-form-urlencoded" },
2686
+ method: "POST"
2687
+ });
2688
+ } catch (cause) {
2689
+ throw new AuthError("AUTH_CODE_EXCHANGE_FAILED", { cause });
2690
+ }
2691
+ if (!response.ok) {
2692
+ throw new AuthError("AUTH_CODE_EXCHANGE_FAILED", {
2693
+ statusCode: response.status
2694
+ });
2695
+ }
2696
+ let payload;
2697
+ try {
2698
+ payload = await response.json();
2699
+ } catch (cause) {
2700
+ throw new AuthError("AUTH_CODE_EXCHANGE_FAILED", { cause });
2701
+ }
2702
+ try {
2703
+ return parseTokenSet(payload);
2704
+ } catch (cause) {
2705
+ if (cause instanceof ContractParseError) {
2706
+ throw new AuthError("AUTH_CODE_EXCHANGE_FAILED", { cause });
2707
+ }
2708
+ throw cause;
2709
+ }
2710
+ };
2711
+
2712
+ // src/auth/refresh-session/refresh-session.ts
2713
+ var refreshSession = async (options) => {
2714
+ const body = new URLSearchParams({
2715
+ client_id: options.clientId,
2716
+ grant_type: "refresh_token",
2717
+ refresh_token: options.refreshToken
2718
+ });
2719
+ let response;
2720
+ try {
2721
+ response = await (options.fetchImpl ?? fetch)(options.tokenUrl, {
2722
+ body,
2723
+ headers: { "content-type": "application/x-www-form-urlencoded" },
2724
+ method: "POST"
2725
+ });
2726
+ } catch (cause) {
2727
+ throw new AuthError("AUTH_REAUTHENTICATION_REQUIRED", { cause });
2728
+ }
2729
+ if (!response.ok) {
2730
+ throw new AuthError("AUTH_REAUTHENTICATION_REQUIRED", {
2731
+ statusCode: response.status
2732
+ });
2733
+ }
2734
+ let payload;
2735
+ try {
2736
+ payload = await response.json();
2737
+ } catch (cause) {
2738
+ throw new AuthError("AUTH_REAUTHENTICATION_REQUIRED", { cause });
2739
+ }
2740
+ try {
2741
+ return parseTokenSet(payload);
2742
+ } catch (cause) {
2743
+ if (cause instanceof ContractParseError) {
2744
+ throw new AuthError("AUTH_REAUTHENTICATION_REQUIRED", { cause });
2745
+ }
2746
+ throw cause;
2747
+ }
2748
+ };
2749
+
2750
+ // src/auth/create-auth-session/create-auth-session.ts
2751
+ var createAuthSession = (options) => {
2752
+ const tokenUrl = new URL("/oauth/token", options.issuerUrl);
2753
+ return {
2754
+ authorizationUrl: (request) => buildAuthorizeUrl(options.issuerUrl, request),
2755
+ exchangeCode: (input) => exchangeCodeAndParse({
2756
+ clientId: options.clientId,
2757
+ ...options.fetchImpl === void 0 ? {} : { fetchImpl: options.fetchImpl },
2758
+ input,
2759
+ tokenUrl
2760
+ }),
2761
+ refresh: (refreshToken) => refreshSession({
2762
+ clientId: options.clientId,
2763
+ ...options.fetchImpl === void 0 ? {} : { fetchImpl: options.fetchImpl },
2764
+ refreshToken,
2765
+ tokenUrl
2766
+ })
2767
+ };
2768
+ };
2769
+
2770
+ // src/auth/open-browser-and-capture/open-browser-and-capture.ts
2771
+ import { timingSafeEqual } from "node:crypto";
2772
+
2773
+ // src/auth/loopback-callback/loopback-callback.ts
2774
+ import { createServer } from "node:http";
2775
+
2776
+ // src/auth/loopback-callback/callback-page/callback-page.ts
2777
+ var MARK_PATHS = [
2778
+ "M310.6,175.8c0-6.1-2.7-11.8-7.2-15.9l-127.6-114.4V7h-43.5v48.3c0,6.2,2.6,12.1,7.2,16.2l92.5,82.9H0v43.5h234.7l-95.6,91.4c-4.3,4.1-6.7,9.8-6.7,15.7v48.3h43.5v-39l128.1-122.4c4.4-4.2,6.8-10,6.7-16.1h0Z",
2779
+ "M457.2,175.9v-2.6c18.4-4.9,33-14.1,44-27.7,11-13.6,16.5-30.3,16.5-50.3s-4.5-32.9-13.4-47.5c-9-14.5-21.6-26.1-38-34.9-16.4-8.7-35.8-13.1-58.3-13.1s-41.1,4-58.8,12.1-31.8,19.3-42.4,33.7-16.1,31-16.6,50h51.1c.3-10.8,3.6-20,9.8-27.7,6.2-7.6,14.2-13.5,24.1-17.5,9.9-4.1,20.6-6.1,32-6.1s22.3,2.2,31.1,6.7c8.8,4.5,15.6,10.8,20.5,18.8,4.9,8.1,7.3,17.5,7.3,28.4s-2.8,21.1-8.4,29.4-13.3,14.8-23.2,19.5-21.3,7.1-34.2,7.1h-29.9v43.3h29.9c16.2,0,30,2.5,41.5,7.5,11.5,5,20.2,12,26.1,21.1,6,9,9,19.4,9,31.2s-3,21.2-9.1,29.8-14.4,15.3-25.1,20.1c-10.7,4.9-22.9,7.3-36.7,7.3s-24.2-2.1-34.5-6.2-18.6-10-24.8-17.5c-6.3-7.5-9.8-16.5-10.6-26.8h-53.2c.6,19,6.3,35.7,17,50.2,10.7,14.5,25.2,25.7,43.6,33.8,18.4,8.1,39.4,12.1,63.1,12.1s45.1-4.4,63.7-13.2c18.6-8.8,33.3-20.8,44-36,10.8-15.2,16.1-32.5,16.1-51.8s-6.4-40.7-19.2-55.6c-12.8-14.9-30.8-24.2-54-27.8h0Z",
2780
+ "M763,203.7c-12.8-14.9-30.8-24.2-54-27.8v-2.6c18.4-4.9,33-14.1,44-27.7,11-13.6,16.5-30.3,16.5-50.3s-4.5-32.9-13.4-47.5c-8.9-14.5-21.6-26.1-38-34.9-16.4-8.7-35.8-13.1-58.3-13.1s-41.1,4-58.8,12.1-31.8,19.3-42.4,33.7-16.1,31-16.6,50h51.1c.3-10.8,3.6-20,9.8-27.7,6.2-7.6,14.2-13.5,24.1-17.5,9.9-4.1,20.6-6.1,32-6.1s22.3,2.2,31.1,6.7c8.8,4.5,15.6,10.8,20.5,18.8,4.9,8.1,7.3,17.5,7.3,28.4s-2.8,21.1-8.4,29.4c-5.6,8.3-13.3,14.8-23.2,19.5s-21.3,7.1-34.2,7.1h-29.9v43.3h29.9c16.2,0,30,2.5,41.5,7.5,11.5,5,20.2,12,26.1,21.1,6,9,8.9,19.4,8.9,31.2s-3,21.2-9.1,29.8c-6.1,8.6-14.4,15.3-25.1,20.1-10.7,4.9-22.9,7.3-36.7,7.3s-24.2-2.1-34.5-6.2-18.6-10-24.8-17.5c-6.3-7.5-9.8-16.5-10.6-26.8h-53.2c.6,19,6.3,35.7,17,50.2,10.7,14.5,25.2,25.7,43.6,33.8,18.4,8.1,39.4,12.1,63.1,12.1s45.1-4.4,63.7-13.2,33.3-20.8,44-36c10.8-15.2,16.1-32.5,16.1-51.8s-6.4-40.7-19.2-55.6h0Z"
2781
+ ];
2782
+ var BRAND_MARK = `<svg class="r33-mark" viewBox="0 0 782.2 360.3" fill="currentColor" role="img" aria-label="route33.ai" xmlns="http://www.w3.org/2000/svg"><path d="${MARK_PATHS[0]}"/><path d="${MARK_PATHS[1]}"/><path d="${MARK_PATHS[2]}"/></svg>`;
2783
+ var PAGE_STYLE = `:root{color-scheme:dark;--r33-euro-blue:#0000FF;--r33-off-white:#F1F1F1;--r33-off-black:#121212;--r33-grey-300:#C9CBCF;--r33-radius-fillet:4px;--r33-space-2:8px;--r33-space-3:16px;--r33-space-4:24px;--r33-space-6:64px;--r33-font-body:"Geist","Helvetica Neue",Arial,sans-serif;--r33-font-mono:"Geist Mono","SFMono-Regular",Menlo,monospace;--r33-surface:var(--r33-off-black);--r33-text-primary:var(--r33-off-white);--r33-text-secondary:var(--r33-grey-300);--r33-fill-accent:var(--r33-euro-blue);--r33-on-fill-accent:var(--r33-off-white)}
2784
+ *{box-sizing:border-box}
2785
+ html,body{margin:0}
2786
+ .r33-page{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:var(--r33-space-6);background:var(--r33-surface);color:var(--r33-text-primary);font-family:var(--r33-font-body);-webkit-font-smoothing:antialiased}
2787
+ .r33-panel{display:grid;gap:var(--r33-space-4);max-width:460px;width:100%}
2788
+ .r33-mark{height:40px;width:auto;color:var(--r33-text-primary)}
2789
+ .r33-status{display:inline-flex;align-items:center;gap:var(--r33-space-2);font-size:11px;letter-spacing:0.08em;text-transform:uppercase;color:var(--r33-text-secondary)}
2790
+ .r33-status__dot{width:8px;height:8px;background:var(--r33-fill-accent)}
2791
+ .r33-status__dot--error{background:transparent;border:1px solid var(--r33-text-primary);border-radius:50%}
2792
+ .r33-headline{margin:0;font-size:48px;line-height:0.85;letter-spacing:-0.02em;font-weight:600;color:var(--r33-text-primary);text-wrap:balance}
2793
+ .r33-subline{margin:0;font-size:16px;line-height:1.45;color:var(--r33-text-secondary);max-width:40ch;text-wrap:pretty}
2794
+ .r33-code{margin:0;font-size:13px;letter-spacing:0.04em;color:var(--r33-text-secondary)}
2795
+ .r33-code__value{font-family:var(--r33-font-mono);text-transform:uppercase;color:var(--r33-text-primary)}
2796
+ @media (prefers-reduced-motion:reduce){*{animation-duration:1ms!important;transition-duration:1ms!important}}`;
2797
+ var buildCallbackPage = (state, errorCode) => {
2798
+ const isError = state === "error";
2799
+ const safeCode = (errorCode ?? "").replace(/[^a-z0-9_-]/gi, "");
2800
+ const title = isError ? "Sign-in failed" : "Signed in";
2801
+ const headline = isError ? "Sign-in failed" : "Signed in to route33";
2802
+ const subline = isError ? "Return to OpenCode and retry." : "Return to OpenCode \u2014 one more step may wait there: if it asks which budget covers usage, pick a project. This window can be closed.";
2803
+ const statusLabel = isError ? "Authentication error" : "Authenticated";
2804
+ const dotClass = isError ? "r33-status__dot r33-status__dot--error" : "r33-status__dot";
2805
+ const codeLine = isError && safeCode.length > 0 ? `<p class="r33-code">Error code: <span class="r33-code__value">${safeCode}</span></p>` : "";
2806
+ return `<!doctype html>
2807
+ <html lang="en" data-theme="dark">
2808
+ <head>
2809
+ <meta charset="utf-8">
2810
+ <meta name="viewport" content="width=device-width, initial-scale=1">
2811
+ <title>${title} \xB7 route33.ai</title>
2812
+ <style>${PAGE_STYLE}</style>
2813
+ </head>
2814
+ <body>
2815
+ <main class="r33-page">
2816
+ <div class="r33-panel">
2817
+ ${BRAND_MARK}
2818
+ <span class="r33-status"><span class="${dotClass}"></span>${statusLabel}</span>
2819
+ <h1 class="r33-headline">${headline}</h1>
2820
+ <p class="r33-subline">${subline}</p>
2821
+ ${codeLine}
2822
+ </div>
2823
+ </main>
2824
+ </body>
2825
+ </html>`;
2826
+ };
2827
+
2828
+ // src/auth/loopback-callback/loopback-callback.ts
2829
+ var createAuthLoopbackCallback = async () => {
2830
+ let redirectUri = "http://127.0.0.1/callback";
2831
+ let resolveCapture;
2832
+ const captured = new Promise((resolve2) => {
2833
+ resolveCapture = resolve2;
2834
+ });
2835
+ const server = createServer((request, response) => {
2836
+ const url = new URL(request.url ?? "/", redirectUri);
2837
+ if (request.method !== "GET" || url.pathname !== "/callback") {
2838
+ response.writeHead(404).end();
2839
+ return;
2840
+ }
2841
+ const resolve2 = resolveCapture;
2842
+ resolveCapture = void 0;
2843
+ resolve2?.(url);
2844
+ const error = url.searchParams.get("error");
2845
+ const page = error === null || error.length === 0 ? buildCallbackPage("success") : buildCallbackPage("error", error);
2846
+ response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
2847
+ response.end(page);
2848
+ });
2849
+ await new Promise((resolve2, reject) => {
2850
+ const onError = (cause) => {
2851
+ reject(new AuthError("AUTH_CALLBACK_LISTEN_FAILED", { cause }));
2852
+ };
2853
+ server.once("error", onError);
2854
+ server.listen(0, "127.0.0.1", () => {
2855
+ server.off("error", onError);
2856
+ resolve2();
2857
+ });
2858
+ });
2859
+ const address = server.address();
2860
+ if (address === null || typeof address === "string") {
2861
+ server.close();
2862
+ throw new AuthError("AUTH_CALLBACK_LISTEN_FAILED");
2863
+ }
2864
+ redirectUri = `http://127.0.0.1:${String(address.port)}/callback`;
2865
+ return {
2866
+ redirectUri,
2867
+ close: () => {
2868
+ if (!server.listening) {
2869
+ return Promise.resolve();
2870
+ }
2871
+ return new Promise((resolve2, reject) => {
2872
+ server.close((cause) => {
2873
+ if (cause === void 0) {
2874
+ resolve2();
2875
+ return;
2876
+ }
2877
+ reject(new AuthError("AUTH_CALLBACK_CLEANUP_FAILED", { cause }));
2878
+ });
2879
+ });
2880
+ },
2881
+ wait: () => captured
2882
+ };
2883
+ };
2884
+
2885
+ // src/auth/open-system-browser/open-system-browser.ts
2886
+ import { spawn } from "node:child_process";
2887
+ var openSystemBrowser = async (url, options = {}) => new Promise((resolve2, reject) => {
2888
+ let command = "xdg-open";
2889
+ let args = [url.href];
2890
+ if (process.platform === "darwin") {
2891
+ command = "open";
2892
+ } else if (process.platform === "win32") {
2893
+ command = "cmd";
2894
+ args = ["/c", "start", "", url.href];
2895
+ }
2896
+ if (options.spawnUrl !== void 0) {
2897
+ options.spawnUrl(command, args, { detached: true, stdio: "ignore" });
2898
+ resolve2();
2899
+ return;
2900
+ }
2901
+ const child = spawn(command, args, {
2902
+ detached: true,
2903
+ stdio: "ignore"
2904
+ });
2905
+ child.once("error", reject);
2906
+ child.once("spawn", () => {
2907
+ child.unref();
2908
+ resolve2();
2909
+ });
2910
+ });
2911
+
2912
+ // src/auth/open-browser-and-capture/open-browser-and-capture.ts
2913
+ var openBrowserAndCapture = async (options) => {
2914
+ const callback = await createAuthLoopbackCallback();
2915
+ const authorizationUrl = options.authorizationUrl(callback.redirectUri);
2916
+ const browserRunner = options.browserRunner ?? ((url) => openSystemBrowser(url));
2917
+ let timer;
2918
+ const timeout = new Promise((_resolve, reject) => {
2919
+ timer = setTimeout(() => {
2920
+ reject(new AuthError("AUTH_CALLBACK_TIMEOUT"));
2921
+ }, options.timeoutMs ?? 12e4);
2922
+ });
2923
+ const launch = browserRunner(authorizationUrl).catch((cause) => {
2924
+ throw new AuthError("AUTH_BROWSER_LAUNCH_FAILED", { cause });
2925
+ });
2926
+ try {
2927
+ const completed = await Promise.race([
2928
+ Promise.all([launch, callback.wait()]),
2929
+ timeout
2930
+ ]);
2931
+ const callbackUrl = completed[1];
2932
+ const actualState = callbackUrl.searchParams.get("state");
2933
+ if (actualState === null) {
2934
+ throw new AuthError("AUTH_CALLBACK_RESPONSE_INVALID");
2935
+ }
2936
+ const expectedState = Buffer.from(options.expectedState);
2937
+ const receivedState = Buffer.from(actualState);
2938
+ if (expectedState.length !== receivedState.length || !timingSafeEqual(expectedState, receivedState)) {
2939
+ throw new AuthError("AUTH_CALLBACK_STATE_MISMATCH");
2940
+ }
2941
+ const code = callbackUrl.searchParams.get("code");
2942
+ if (code === null || code.length === 0) {
2943
+ throw new AuthError("AUTH_CALLBACK_RESPONSE_INVALID");
2944
+ }
2945
+ return { code, redirectUri: callback.redirectUri };
2946
+ } finally {
2947
+ if (timer !== void 0) {
2948
+ clearTimeout(timer);
2949
+ }
2950
+ await callback.close();
2951
+ }
2952
+ };
2953
+
2954
+ // src/auth/pkce-verifier-and-challenge/pkce-verifier-and-challenge.ts
2955
+ import { randomBytes } from "node:crypto";
2956
+
2957
+ // src/auth/pkce-challenge/pkce-challenge.ts
2958
+ import { createHash as createHash3 } from "node:crypto";
2959
+ var pkceChallenge = (verifier) => createHash3("sha256").update(verifier).digest("base64url");
2960
+
2961
+ // src/auth/pkce-verifier-and-challenge/pkce-verifier-and-challenge.ts
2962
+ var createPkceVerifierAndChallenge = () => {
2963
+ const codeVerifier = randomBytes(32).toString("base64url");
2964
+ return {
2965
+ codeChallenge: pkceChallenge(codeVerifier),
2966
+ codeVerifier,
2967
+ state: randomBytes(32).toString("base64url")
2968
+ };
2969
+ };
2970
+
2971
+ // src/auth/requested-scopes/requested-scopes.ts
2972
+ var REQUESTED_OAUTH_SCOPES = "openid offline_access orgs:read projects:read keys:read keys:write";
2973
+
2974
+ // src/auth/login-flow/login-flow.ts
2975
+ var loginFlow = async (options) => {
2976
+ const log = options.logger ?? logger;
2977
+ log.info("auth.login.started");
2978
+ const pkce = createPkceVerifierAndChallenge();
2979
+ const captured = await openBrowserAndCapture({
2980
+ authorizationUrl: (redirectUri) => options.session.authorizationUrl({
2981
+ clientId: options.clientId,
2982
+ codeChallenge: pkce.codeChallenge,
2983
+ redirectUri,
2984
+ scope: REQUESTED_OAUTH_SCOPES,
2985
+ state: pkce.state
2986
+ }),
2987
+ ...options.browserRunner === void 0 ? {} : { browserRunner: options.browserRunner },
2988
+ expectedState: pkce.state,
2989
+ ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
2990
+ });
2991
+ const tokenSet = await options.session.exchangeCode({
2992
+ code: captured.code,
2993
+ codeVerifier: pkce.codeVerifier,
2994
+ redirectUri: captured.redirectUri
2995
+ });
2996
+ let credential;
2997
+ try {
2998
+ credential = parseManagementJwt(tokenSet.access_token);
2999
+ } catch (cause) {
3000
+ throw new AuthError("AUTH_TOKEN_RESPONSE_INVALID", { cause });
3001
+ }
3002
+ log.info("auth.login.completed");
3003
+ return { credential, tokenSet };
3004
+ };
3005
+
3006
+ // src/route33-runtime/login-session/login-session.ts
3007
+ var EXPIRY_SKEW_MS = 6e4;
3008
+ var LoginSession = class {
3009
+ #options;
3010
+ #result;
3011
+ #expiresAt;
3012
+ constructor(options) {
3013
+ this.#options = options;
3014
+ }
3015
+ /**
3016
+ * Resolves the login once per process, sharing in-flight attempts.
3017
+ * @param policy - Whether this attempt may start a browser login; an in-flight attempt is always shared.
3018
+ * @returns The token set plus its typed management credential.
3019
+ */
3020
+ result(policy = {}) {
3021
+ if (this.#result !== void 0 && this.#expiresAt !== void 0 && Date.now() >= this.#expiresAt - EXPIRY_SKEW_MS) {
3022
+ this.#result = void 0;
3023
+ }
3024
+ this.#result ??= this.#start(policy).catch((cause) => {
3025
+ this.#result = void 0;
3026
+ throw cause;
3027
+ });
3028
+ return this.#result;
3029
+ }
3030
+ /**
3031
+ * Resolves only the management credential for lazy management clients.
3032
+ * @param policy - Whether this attempt may start a browser login.
3033
+ * @returns The current login's typed management credential.
3034
+ */
3035
+ async credential(policy = {}) {
3036
+ return (await this.result(policy)).credential;
3037
+ }
3038
+ async #start(policy) {
3039
+ const viaRefresh = await this.#tryRefresh();
3040
+ if (viaRefresh !== void 0) {
3041
+ return viaRefresh;
3042
+ }
3043
+ if (policy.allowBrowser === false) {
3044
+ throw new AuthError("AUTH_REAUTHENTICATION_REQUIRED");
3045
+ }
3046
+ await this.#options.presenter?.present(TRUSTED_PARTY_COPY);
3047
+ const result = await loginFlow(this.#options.auth);
3048
+ this.#trackExpiry(result.tokenSet.expires_in);
3049
+ await this.#persist(result.tokenSet.refresh_token);
3050
+ return result;
3051
+ }
3052
+ async #tryRefresh() {
3053
+ const tokenStore = this.#options.tokenStore;
3054
+ if (tokenStore === void 0) {
3055
+ return void 0;
3056
+ }
3057
+ const stored = await tokenStore.get();
3058
+ if (stored === void 0 || stored === "") {
3059
+ return void 0;
3060
+ }
3061
+ let tokenSet;
3062
+ try {
3063
+ tokenSet = await this.#options.auth.session.refresh(stored);
3064
+ } catch {
3065
+ logger.info("auth.refresh.fell-back-to-browser");
3066
+ return void 0;
3067
+ }
3068
+ let credential;
3069
+ try {
3070
+ credential = parseManagementJwt(tokenSet.access_token);
3071
+ } catch (cause) {
3072
+ throw new AuthError("AUTH_TOKEN_RESPONSE_INVALID", { cause });
3073
+ }
3074
+ this.#trackExpiry(tokenSet.expires_in);
3075
+ await this.#persist(tokenSet.refresh_token);
3076
+ return { credential, tokenSet };
3077
+ }
3078
+ #trackExpiry(expiresIn) {
3079
+ this.#expiresAt = Date.now() + expiresIn * 1e3;
3080
+ }
3081
+ async #persist(token) {
3082
+ try {
3083
+ await this.#options.tokenStore?.set(token);
3084
+ } catch {
3085
+ logger.warn("auth.token.persist-failed");
3086
+ }
3087
+ }
3088
+ };
3089
+
3090
+ // src/route33-runtime/route33-runtime.ts
3091
+ var Route33Runtime = class {
3092
+ #options;
3093
+ #session;
3094
+ #managementClient;
3095
+ constructor(options) {
3096
+ this.#options = options;
3097
+ this.#session = new LoginSession({
3098
+ auth: options.auth,
3099
+ ...options.presenter === void 0 ? {} : { presenter: options.presenter },
3100
+ ...options.tokenStore === void 0 ? {} : { tokenStore: options.tokenStore }
3101
+ });
3102
+ }
3103
+ /**
3104
+ * Runs the browser or refresh-token login once for this process.
3105
+ * @param policy - Whether this attempt may start a browser login.
3106
+ * @returns The token set plus its typed management credential.
3107
+ */
3108
+ login(policy = {}) {
3109
+ return this.#session.result(policy);
3110
+ }
3111
+ /**
3112
+ * Lists projects visible to the current management login.
3113
+ * @returns Projects with their listing organisation stitched in.
3114
+ */
3115
+ async listProjects() {
3116
+ const credential = await this.#session.credential();
3117
+ const client = this.#client();
3118
+ const organizations = await client.listOrganizations(credential);
3119
+ const projects = await Promise.all(
3120
+ organizations.data.map(async (organization) => {
3121
+ const collection = await client.listProjects(
3122
+ credential,
3123
+ organization.id
3124
+ );
3125
+ return collection.data.map((project) => ({
3126
+ ...project,
3127
+ organisation: { id: organization.id, name: organization.name }
3128
+ }));
3129
+ })
3130
+ );
3131
+ return projects.flat();
3132
+ }
3133
+ /**
3134
+ * Reads the stored key for a saved attribution without any login.
3135
+ * @returns The reusable key and lease when the stored lease is still valid.
3136
+ */
3137
+ async storedReusableKey() {
3138
+ const attribution = readAttribution(this.#options.state);
3139
+ if (attribution === void 0) {
3140
+ return void 0;
3141
+ }
3142
+ const installation = this.#installation(attribution);
3143
+ await this.#hydrateState(installation);
3144
+ const storedKey = await installation.secretStore.get();
3145
+ return reusableLease({
3146
+ attribution,
3147
+ current: installation.state.snapshot(),
3148
+ now: this.#now(),
3149
+ policy: resolveLeasePolicy(this.#options.policy),
3150
+ state: installation.state,
3151
+ storedKey
3152
+ });
3153
+ }
3154
+ /**
3155
+ * Resolves the current gateway credential for the config hook.
3156
+ * @returns A leased inference key.
3157
+ */
3158
+ async resolveCredential() {
3159
+ return (await this.#lease(this.#options.policy)).key;
3160
+ }
3161
+ /**
3162
+ * Resolves the workspace-owned key for any directory without prompting.
3163
+ * @param directory - Session directory whose worktree attribution decides the key.
3164
+ * @returns That workspace's leased key, or undefined when it never connected.
3165
+ */
3166
+ async resolveWorkspaceCredential(directory) {
3167
+ const attribution = readAttribution({
3168
+ directory,
3169
+ outsideWorktree: "return-undefined"
3170
+ });
3171
+ if (attribution === void 0) {
3172
+ return void 0;
3173
+ }
3174
+ return (await this.#leaseAttribution(attribution, void 0, directory)).key;
3175
+ }
3176
+ /**
3177
+ * Builds injectable handlers for the host's command surface.
3178
+ * @returns Change, reset, rotate, and safe inspection handlers.
3179
+ */
3180
+ commandHandlers() {
3181
+ return {
3182
+ "change-attribution": () => changeAttribution({
3183
+ clear: () => {
3184
+ clearAttribution(this.#options.state);
3185
+ },
3186
+ prompt: () => this.#resolveAttribution()
3187
+ }),
3188
+ "inspect-session": () => inspectSession({
3189
+ readAttribution: () => readAttribution(this.#options.state),
3190
+ readLease: () => {
3191
+ const attribution = readAttribution(this.#options.state);
3192
+ return attribution === void 0 ? void 0 : this.#installation(attribution).state.snapshot();
3193
+ }
3194
+ }),
3195
+ "reset-attribution": () => resetAttribution({
3196
+ clear: () => {
3197
+ clearAttribution(this.#options.state);
3198
+ }
3199
+ }),
3200
+ "rotate-now": () => rotateNow({
3201
+ lease: (policy) => this.#lease({ ...this.#options.policy, ...policy })
3202
+ })
3203
+ };
3204
+ }
3205
+ #client() {
3206
+ const client = this.#options.management.client;
3207
+ if (typeof client !== "function") {
3208
+ return client;
3209
+ }
3210
+ this.#managementClient ??= client();
3211
+ return this.#managementClient;
3212
+ }
3213
+ #subject(credential) {
3214
+ const subject = this.#options.management.subject;
3215
+ if (subject === void 0) {
3216
+ return parseManagementSubject(credential);
3217
+ }
3218
+ return typeof subject === "string" ? subject : subject(credential);
3219
+ }
3220
+ #now() {
3221
+ return (this.#options.clock ?? Date.now)();
3222
+ }
3223
+ async #resolveAttribution() {
3224
+ return resolveAttribution({
3225
+ loadProjects: async () => {
3226
+ await this.#session.credential();
3227
+ return this.listProjects();
3228
+ },
3229
+ ...this.#options.presenter === void 0 ? {} : { presenter: this.#options.presenter },
3230
+ state: this.#options.state
3231
+ });
3232
+ }
3233
+ async #lease(policy) {
3234
+ const attribution = await this.#resolveAttribution();
3235
+ return this.#leaseAttribution(attribution, policy);
3236
+ }
3237
+ async #leaseAttribution(attribution, policy, directory = this.#options.state.directory) {
3238
+ const installation = this.#installation(attribution, directory);
3239
+ await this.#hydrateState(installation);
3240
+ const minted = await leaseForAttribution({
3241
+ attribution,
3242
+ ...this.#options.clock === void 0 ? {} : { clock: this.#options.clock },
3243
+ installation,
3244
+ management: {
3245
+ client: this.#client(),
3246
+ credential: () => this.#session.credential(),
3247
+ subject: async () => this.#subject(await this.#session.credential())
3248
+ },
3249
+ ...policy === void 0 ? {} : { policy }
3250
+ });
3251
+ await this.#persistState(installation, minted.lease);
3252
+ return minted;
3253
+ }
3254
+ async #hydrateState(installation) {
3255
+ const store = installation.stateStore;
3256
+ if (store === void 0 || installation.state.snapshot() !== void 0) {
3257
+ return;
3258
+ }
3259
+ try {
3260
+ const persisted = await store.get();
3261
+ if (persisted !== void 0) {
3262
+ installation.state.compareAndSwap(void 0, persisted);
3263
+ }
3264
+ } catch {
3265
+ }
3266
+ }
3267
+ async #persistState(installation, lease) {
3268
+ try {
3269
+ await installation.stateStore?.set(lease);
3270
+ } catch {
3271
+ }
3272
+ }
3273
+ #installation(attribution, directory = this.#options.state.directory) {
3274
+ switch (attribution.kind) {
3275
+ case "personal":
3276
+ return this.#options.installation(deriveScopeId({ attribution }));
3277
+ case "project": {
3278
+ const folderRealpath = detectWorktreeRoot(directory);
3279
+ if (folderRealpath === void 0) {
3280
+ throw new TypeError("Project lease scope requires a worktree root");
3281
+ }
3282
+ return this.#options.installation(
3283
+ deriveScopeId({ attribution, folderRealpath })
3284
+ );
3285
+ }
3286
+ default: {
3287
+ const unexpected = attribution;
3288
+ throw new TypeError(`Unexpected attribution: ${String(unexpected)}`);
3289
+ }
3290
+ }
3291
+ }
3292
+ };
3293
+
3294
+ // src/v1-entry/unattributed-prompt-gate/unattributed-prompt-gate.ts
3295
+ var createUnattributedPromptGate = ({ directory, outsideWorktree }) => (input) => {
3296
+ if (input.model?.providerID !== "route_33") return Promise.resolve();
3297
+ const attribution = readAttribution({ directory, outsideWorktree });
3298
+ if (attribution !== void 0) return Promise.resolve();
3299
+ return Promise.reject(
3300
+ new Error(
3301
+ "Route 33 needs a budget for this folder first \u2014 run /r33 in OpenCode, choose Route 33, and assign a budget."
3302
+ )
3303
+ );
3304
+ };
3305
+
3306
+ // src/v1-entry/workspace-key-headers/workspace-key-headers.ts
3307
+ var createWorkspaceKeyHeaders = ({ resolveKey, session }) => async (input, output) => {
3308
+ const workspace = await session.get({ sessionID: input.sessionID });
3309
+ const key = await resolveKey(workspace.location.directory);
3310
+ if (key === void 0) return;
3311
+ output.headers["Authorization"] = `Bearer ${key.value}`;
3312
+ };
3313
+
3314
+ // src/create-route33-plugin/create-route33-plugin.ts
3315
+ var createRoute33Plugin = (options = {}) => {
3316
+ const baseURL = readEnvBaseUrl();
3317
+ const clientId = options.clientId ?? readEnvClientId() ?? DEFAULT_ROUTE33_CLIENT_ID;
3318
+ const directKey = readEnvDirectApiKey();
3319
+ const resolvedConfig = {
3320
+ ...baseURL === void 0 ? {} : { baseURL },
3321
+ cacheDir: readEnvCacheDir(),
3322
+ catalogTtlMs: readEnvCatalogTtlMs(),
3323
+ disableIdShim: readEnvDisableIdShim(),
3324
+ discoveryEnabled: readEnvDiscovery(),
3325
+ strict: readEnvStrict(),
3326
+ ...options.config
3327
+ };
3328
+ const runtimeOptions = options.runtime;
3329
+ const installation = runtimeOptions === void 0 ? void 0 : runtimeOptions.installation ?? createLeaseInstallationFactory({
3330
+ createSecretStore: (scopeId) => new FileSecretStore({
3331
+ directory: resolveV1SecretStoreDirectory({
3332
+ ...options.secretStoreDirectory === void 0 ? {} : { directory: options.secretStoreDirectory }
3333
+ }),
3334
+ scopeId
3335
+ })
3336
+ });
3337
+ return (input) => {
3338
+ const runtime = runtimeOptions === void 0 || installation === void 0 ? void 0 : new Route33Runtime({
3339
+ ...runtimeOptions,
3340
+ auth: { ...runtimeOptions.auth, clientId },
3341
+ installation,
3342
+ state: {
3343
+ directory: input.worktree,
3344
+ outsideWorktree: "return-undefined"
3345
+ }
3346
+ });
3347
+ if (runtime !== void 0) {
3348
+ options.bindRuntime?.(runtime);
3349
+ if (options.registerCommands !== void 0) {
3350
+ options.registerCommands(runtime.commandHandlers());
3351
+ }
3352
+ }
3353
+ let credentialProvider;
3354
+ let workspaceHeaders;
3355
+ let promptGate;
3356
+ if (directKey !== void 0) {
3357
+ credentialProvider = () => Promise.resolve(parseGatewayKey(directKey));
3358
+ } else if (runtime === void 0) {
3359
+ credentialProvider = () => Promise.reject(
3360
+ new AttributionPromptError("ATTRIBUTION_PRESENTER_UNAVAILABLE")
3361
+ );
3362
+ } else {
3363
+ credentialProvider = () => runtime.resolveCredential();
3364
+ workspaceHeaders = createWorkspaceKeyHeaders({
3365
+ resolveKey: (directory) => runtime.resolveWorkspaceCredential(directory),
3366
+ session: {
3367
+ get: async ({ sessionID }) => {
3368
+ const session = await options.readSession?.(sessionID);
3369
+ return session ?? { location: { directory: input.worktree } };
3370
+ }
3371
+ }
3372
+ });
3373
+ promptGate = createUnattributedPromptGate({
3374
+ directory: input.worktree,
3375
+ outsideWorktree: "return-undefined"
3376
+ });
3377
+ }
3378
+ return Promise.resolve({
3379
+ config: createConfigHook({
3380
+ ...resolvedConfig,
3381
+ credentialProvider,
3382
+ cwd: input.worktree
3383
+ }),
3384
+ ...workspaceHeaders === void 0 ? {} : { "chat.headers": workspaceHeaders },
3385
+ ...promptGate === void 0 ? {} : { "chat.message": promptGate }
3386
+ });
3387
+ };
3388
+ };
3389
+
3390
+ // src/contract/management-keys/parse-api-key/parse-api-key.ts
3391
+ var parseApiKey = (value) => {
3392
+ if (!isRecord(value) || !isOwnerType(value["ownership_type"]) || !isNullableString(value["project_id"])) {
3393
+ throw new ContractParseError("invalid API key");
3394
+ }
3395
+ const field = (name) => {
3396
+ const text = value[name];
3397
+ if (typeof text !== "string") {
3398
+ throw new ContractParseError("invalid API key");
3399
+ }
3400
+ return text;
3401
+ };
3402
+ const status = value["status"];
3403
+ if (status !== "active" && status !== "revoked") {
3404
+ throw new ContractParseError("invalid API key");
3405
+ }
3406
+ return {
3407
+ expires_at: field("expires_at"),
3408
+ id: field("id"),
3409
+ name: field("name"),
3410
+ org_id: field("org_id"),
3411
+ ownership_type: value["ownership_type"],
3412
+ project_id: value["project_id"],
3413
+ revoked: value["revoked"] === true,
3414
+ status
3415
+ };
3416
+ };
3417
+
3418
+ // src/contract/management-keys/parse-collection/parse-collection.ts
3419
+ var parseCollection = (value, parseItem) => {
3420
+ if (!isRecord(value)) {
3421
+ throw new ContractParseError("invalid management collection");
3422
+ }
3423
+ const data = value["data"];
3424
+ if (!Array.isArray(data)) {
3425
+ throw new ContractParseError("invalid management collection");
3426
+ }
3427
+ const page = value["page"];
3428
+ if (!isRecord(page)) {
3429
+ throw new ContractParseError("invalid management collection");
3430
+ }
3431
+ const limit = page["limit"];
3432
+ if (typeof limit !== "number") {
3433
+ throw new ContractParseError("invalid management collection");
3434
+ }
3435
+ if (![Number.isInteger(limit), limit >= 1, limit <= 500].every(Boolean)) {
3436
+ throw new ContractParseError("invalid management collection");
3437
+ }
3438
+ const nextCursor = page["next_cursor"];
3439
+ if (!isNullableString(nextCursor)) {
3440
+ throw new ContractParseError("invalid management collection");
3441
+ }
3442
+ const hasMore = page["has_more"];
3443
+ if (typeof hasMore !== "boolean") {
3444
+ throw new ContractParseError("invalid management collection");
3445
+ }
3446
+ const collection = {
3447
+ data: data.map(parseItem),
3448
+ page: {
3449
+ has_more: hasMore,
3450
+ limit,
3451
+ next_cursor: nextCursor
3452
+ }
3453
+ };
3454
+ const meta = value["meta"];
3455
+ if (meta === void 0) {
3456
+ return collection;
3457
+ }
3458
+ if (!isRecord(meta)) {
3459
+ throw new ContractParseError("invalid management collection");
3460
+ }
3461
+ return { ...collection, meta };
3462
+ };
3463
+
3464
+ // src/contract/management-keys/parse-key-material/parse-key-material.ts
3465
+ var parseKeyMaterial = (value) => {
3466
+ if (!isRecord(value) || typeof value["key"] !== "string") {
3467
+ throw new ContractParseError("invalid key material");
3468
+ }
3469
+ return { ...parseApiKey(value), key: new GatewayApiKey(value["key"]) };
3470
+ };
3471
+
3472
+ // src/contract/management-keys/parse-organization/parse-organization.ts
3473
+ var parseOrganization = (value) => {
3474
+ if (!isRecord(value)) {
3475
+ throw new ContractParseError("invalid organization");
3476
+ }
3477
+ const stringField = (name) => {
3478
+ const field = value[name];
3479
+ if (typeof field !== "string") {
3480
+ throw new ContractParseError("invalid organization");
3481
+ }
3482
+ return field;
3483
+ };
3484
+ const nullableStringField = (name) => {
3485
+ const field = value[name];
3486
+ if (!isNullableString(field)) {
3487
+ throw new ContractParseError("invalid organization");
3488
+ }
3489
+ return field;
3490
+ };
3491
+ const numberField = (name) => {
3492
+ const field = value[name];
3493
+ if (typeof field !== "number") {
3494
+ throw new ContractParseError("invalid organization");
3495
+ }
3496
+ return field;
3497
+ };
3498
+ const nullableNumberField = (name) => {
3499
+ const field = value[name];
3500
+ if (typeof field !== "number" && field !== null) {
3501
+ throw new ContractParseError("invalid organization");
3502
+ }
3503
+ return field;
3504
+ };
3505
+ const booleanField = (name) => {
3506
+ const field = value[name];
3507
+ if (typeof field !== "boolean") {
3508
+ throw new ContractParseError("invalid organization");
3509
+ }
3510
+ return field;
3511
+ };
3512
+ const budgetField = (name) => {
3513
+ const field = value[name];
3514
+ if (field === null) {
3515
+ return null;
3516
+ }
3517
+ if (!isRecord(field) || typeof field["amount"] !== "string" || typeof field["currency"] !== "string") {
3518
+ throw new ContractParseError("invalid organization");
3519
+ }
3520
+ return { amount: field["amount"], currency: field["currency"] };
3521
+ };
3522
+ const setupStatus = value["setup_status"];
3523
+ if (setupStatus !== "broken" && setupStatus !== "ok" && setupStatus !== "partial" && setupStatus !== "unvalidated") {
3524
+ throw new ContractParseError("invalid organization");
3525
+ }
3526
+ return {
3527
+ alert_active: booleanField("alert_active"),
3528
+ alert_percentage: numberField("alert_percentage"),
3529
+ azure_group_id: nullableStringField("azure_group_id"),
3530
+ azure_owners_group_id: nullableStringField("azure_owners_group_id"),
3531
+ created_at: nullableStringField("created_at"),
3532
+ deactivated_at: nullableStringField("deactivated_at"),
3533
+ default_project_budget: budgetField("default_project_budget"),
3534
+ default_user_budget: budgetField("default_user_budget"),
3535
+ domain: nullableStringField("domain"),
3536
+ id: stringField("id"),
3537
+ idp_connection: nullableStringField("idp_connection"),
3538
+ member_count: nullableNumberField("member_count"),
3539
+ name: stringField("name"),
3540
+ scim_sync: nullableStringField("scim_sync"),
3541
+ setup_status: setupStatus,
3542
+ slug: nullableStringField("slug"),
3543
+ tenant_id: nullableStringField("tenant_id"),
3544
+ updated_at: nullableStringField("updated_at")
3545
+ };
3546
+ };
3547
+
3548
+ // src/contract/management-keys/parse-project/parse-project.ts
3549
+ var parseProject = (value) => {
3550
+ if (!isRecord(value) || !hasStringFields(value, ["id", "name"]) || !hasStringFields(value, ["id", "name", "org_id"])) {
3551
+ throw new ContractParseError("invalid project");
3552
+ }
3553
+ return {
3554
+ id: value["id"] ?? "",
3555
+ name: value["name"] ?? "",
3556
+ org_id: value["org_id"] ?? ""
3557
+ };
3558
+ };
3559
+
3560
+ // src/management/management-error/management-error.ts
3561
+ var ManagementError = class extends Error {
3562
+ constructor(code, options) {
3563
+ super(
3564
+ "Route 33 management request failed",
3565
+ options?.cause === void 0 ? void 0 : { cause: "Route 33 management dependency failed" }
3566
+ );
3567
+ this.code = code;
3568
+ this.name = "ManagementError";
3569
+ this.statusCode = options?.statusCode;
3570
+ }
3571
+ code;
3572
+ statusCode;
3573
+ };
3574
+
3575
+ // src/management/management-http-transport/management-http-transport.ts
3576
+ var ManagementHttpTransport = class {
3577
+ #baseUrl;
3578
+ #credential;
3579
+ #fetchImpl;
3580
+ #onUnauthorized;
3581
+ #timeoutMs;
3582
+ constructor(options) {
3583
+ const baseUrl = options.baseUrl ?? "https://api.route33.ai/v1";
3584
+ this.#baseUrl = new URL(baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
3585
+ this.#credential = options.credential;
3586
+ this.#fetchImpl = options.fetchImpl ?? fetch;
3587
+ this.#onUnauthorized = options.onUnauthorized;
3588
+ this.#timeoutMs = options.timeoutMs ?? 1e4;
3589
+ }
3590
+ /** Sends a request and reads one JSON value under the request timeout.
3591
+ * @param request - Management path, method, and optional JSON body.
3592
+ * @returns The untrusted response value.
3593
+ */
3594
+ json(request) {
3595
+ return this.#perform(request, async (response) => {
3596
+ try {
3597
+ return await response.json();
3598
+ } catch (cause) {
3599
+ throw new ContractParseError("invalid management response", { cause });
3600
+ }
3601
+ });
3602
+ }
3603
+ /** Sends a request whose successful response has no body.
3604
+ * @param request - Management path and method.
3605
+ */
3606
+ empty(request) {
3607
+ return this.#perform(request, () => Promise.resolve());
3608
+ }
3609
+ async #perform(request, consume) {
3610
+ const controller = new AbortController();
3611
+ const timer = setTimeout(() => {
3612
+ controller.abort();
3613
+ }, this.#timeoutMs);
3614
+ const headers = new Headers({
3615
+ accept: "application/json",
3616
+ authorization: `Bearer ${this.#credential.value}`
3617
+ });
3618
+ if (request.body !== void 0) {
3619
+ headers.set("content-type", "application/json");
3620
+ }
3621
+ try {
3622
+ const send = () => this.#fetchImpl(new URL(request.path, this.#baseUrl), {
3623
+ ...request.body === void 0 ? {} : { body: JSON.stringify(request.body) },
3624
+ headers,
3625
+ method: request.method,
3626
+ signal: controller.signal
3627
+ });
3628
+ let response = await send();
3629
+ if (response.status === 401 && this.#onUnauthorized !== void 0) {
3630
+ await this.#onUnauthorized();
3631
+ response = await send();
3632
+ }
3633
+ if (!response.ok) {
3634
+ throw new ManagementError(
3635
+ response.status === 401 ? "MANAGEMENT_UNAUTHORIZED" : "MANAGEMENT_HTTP_ERROR",
3636
+ { statusCode: response.status }
3637
+ );
3638
+ }
3639
+ return await consume(response);
3640
+ } catch (cause) {
3641
+ if (cause instanceof ManagementError) {
3642
+ throw cause;
3643
+ }
3644
+ if (controller.signal.aborted) {
3645
+ throw new ManagementError("MANAGEMENT_TIMEOUT", { cause });
3646
+ }
3647
+ if (cause instanceof ContractParseError) {
3648
+ throw cause;
3649
+ }
3650
+ throw new ManagementError("MANAGEMENT_NETWORK_ERROR", { cause });
3651
+ } finally {
3652
+ clearTimeout(timer);
3653
+ }
3654
+ }
3655
+ };
3656
+
3657
+ // src/management/http-management-keys/http-management-keys.ts
3658
+ var HttpManagementKeys = class {
3659
+ #credential;
3660
+ #transport;
3661
+ constructor(options) {
3662
+ this.#credential = options.credential;
3663
+ this.#transport = new ManagementHttpTransport(options);
3664
+ }
3665
+ /** Creates a future-expiring ownership-scoped key.
3666
+ * @param accessToken - Credential required by the frozen contract.
3667
+ * @param input - Key name, expiry, and ownership.
3668
+ * @returns One-time key material.
3669
+ */
3670
+ async create(accessToken, input) {
3671
+ this.#authorize(accessToken);
3672
+ return parseKeyMaterial(
3673
+ await this.#transport.json({
3674
+ body: {
3675
+ expires_at: input.expiresAt,
3676
+ name: input.name,
3677
+ ...input.ownerId === null ? {} : { owner_user_id: input.ownerId },
3678
+ owner_type: input.ownerType,
3679
+ ...input.projectId === null ? {} : { project_id: input.projectId }
3680
+ },
3681
+ method: "POST",
3682
+ path: "api-keys"
3683
+ })
3684
+ );
3685
+ }
3686
+ /** Treats a successful empty response as confirmation of permanent removal.
3687
+ * @param accessToken - Credential required by the frozen contract.
3688
+ * @param id - Key identifier.
3689
+ */
3690
+ async delete(accessToken, id) {
3691
+ this.#authorize(accessToken);
3692
+ await this.#transport.empty({
3693
+ method: "DELETE",
3694
+ path: `api-keys/${encodeURIComponent(id)}`
3695
+ });
3696
+ }
3697
+ /** Gets a safe key DTO.
3698
+ * @param accessToken - Credential required by the frozen contract.
3699
+ * @param id - Key identifier.
3700
+ * @returns The secret-free key DTO.
3701
+ */
3702
+ async get(accessToken, id) {
3703
+ this.#authorize(accessToken);
3704
+ return parseApiKey(
3705
+ await this.#transport.json({
3706
+ method: "GET",
3707
+ path: `api-keys/${encodeURIComponent(id)}`
3708
+ })
3709
+ );
3710
+ }
3711
+ /** Lists safe key DTOs.
3712
+ * @param accessToken - Credential required by the frozen contract.
3713
+ * @returns The parsed key collection.
3714
+ */
3715
+ async list(accessToken) {
3716
+ this.#authorize(accessToken);
3717
+ return parseCollection(
3718
+ await this.#transport.json({ method: "GET", path: "api-keys" }),
3719
+ parseApiKey
3720
+ );
3721
+ }
3722
+ /** Lists organizations visible to the authenticated management subject.
3723
+ * @param accessToken - Credential required by the frozen contract.
3724
+ * @returns The parsed organization collection.
3725
+ */
3726
+ async listOrganizations(accessToken) {
3727
+ this.#authorize(accessToken);
3728
+ return parseCollection(
3729
+ await this.#transport.json({ method: "GET", path: "orgs" }),
3730
+ parseOrganization
3731
+ );
3732
+ }
3733
+ /** Lists projects visible within an organisation filter.
3734
+ * @param accessToken - Credential required by the frozen contract.
3735
+ * @param organisationId - Required backend organisation filter.
3736
+ * @returns The parsed project collection.
3737
+ */
3738
+ async listProjects(accessToken, organisationId) {
3739
+ this.#authorize(accessToken);
3740
+ if (organisationId.trim().length === 0) {
3741
+ throw new ManagementError("MANAGEMENT_ORGANISATION_ID_REQUIRED", {
3742
+ statusCode: 400
3743
+ });
3744
+ }
3745
+ const query = new URLSearchParams({ org_id: organisationId });
3746
+ return parseCollection(
3747
+ await this.#transport.json({
3748
+ method: "GET",
3749
+ path: `projects?${query.toString()}`
3750
+ }),
3751
+ parseProject
3752
+ );
3753
+ }
3754
+ /** Regenerates one-time key material while preserving expiry when omitted.
3755
+ * @param accessToken - Credential required by the frozen contract.
3756
+ * @param id - Key identifier.
3757
+ * @param expiresAt - Optional replacement expiry.
3758
+ * @returns Regenerated one-time key material.
3759
+ */
3760
+ async regenerate(accessToken, id, expiresAt) {
3761
+ this.#authorize(accessToken);
3762
+ return parseKeyMaterial(
3763
+ await this.#transport.json({
3764
+ body: expiresAt === void 0 ? {} : { expires_at: expiresAt },
3765
+ method: "POST",
3766
+ path: `api-keys/${encodeURIComponent(id)}/secrets`
3767
+ })
3768
+ );
3769
+ }
3770
+ /** Updates mutable key metadata.
3771
+ * @param accessToken - Credential required by the frozen contract.
3772
+ * @param id - Key identifier.
3773
+ * @param input - Optional name and expiry replacements.
3774
+ * @returns The updated safe key DTO.
3775
+ */
3776
+ async update(accessToken, id, input) {
3777
+ this.#authorize(accessToken);
3778
+ return parseApiKey(
3779
+ await this.#transport.json({
3780
+ body: {
3781
+ ...input.expiresAt === void 0 ? {} : { expires_at: input.expiresAt },
3782
+ ...input.name === void 0 ? {} : { name: input.name }
3783
+ },
3784
+ method: "PATCH",
3785
+ path: `api-keys/${encodeURIComponent(id)}`
3786
+ })
3787
+ );
3788
+ }
3789
+ #authorize(accessToken) {
3790
+ if (accessToken.value !== this.#credential.value) {
3791
+ throw new ManagementError("MANAGEMENT_UNAUTHORIZED", {
3792
+ statusCode: 401
3793
+ });
3794
+ }
3795
+ }
3796
+ };
3797
+
3798
+ // src/lazy-management-keys/lazy-management-keys.ts
3799
+ var lazyManagementKeys = (options) => {
3800
+ const client = (accessToken) => new HttpManagementKeys({
3801
+ baseUrl: options.apiBaseUrl,
3802
+ credential: accessToken,
3803
+ ...options.fetchImpl === void 0 ? {} : { fetchImpl: options.fetchImpl }
3804
+ });
3805
+ return {
3806
+ create: async (accessToken, input) => client(accessToken).create(accessToken, input),
3807
+ delete: async (accessToken, id) => client(accessToken).delete(accessToken, id),
3808
+ get: async (accessToken, id) => client(accessToken).get(accessToken, id),
3809
+ list: async (accessToken) => client(accessToken).list(accessToken),
3810
+ listOrganizations: async (accessToken) => client(accessToken).listOrganizations(accessToken),
3811
+ listProjects: async (accessToken, organisationId) => client(accessToken).listProjects(accessToken, organisationId),
3812
+ regenerate: async (accessToken, id, expiresAt) => client(accessToken).regenerate(accessToken, id, expiresAt),
3813
+ update: async (accessToken, id, input) => client(accessToken).update(accessToken, id, input)
3814
+ };
3815
+ };
3816
+
3817
+ // src/production-runtime-options/production-runtime-options.ts
3818
+ var LOGIN_BUDGET_MS = 8.5 * 60 * 1e3;
3819
+ var buildProductionRuntimeOptions = (input) => ({
3820
+ auth: {
3821
+ ...input.browserRunner === void 0 ? {} : { browserRunner: input.browserRunner },
3822
+ session: createAuthSession({
3823
+ clientId: input.clientId,
3824
+ issuerUrl: input.issuerUrl
3825
+ }),
3826
+ timeoutMs: LOGIN_BUDGET_MS
3827
+ },
3828
+ management: {
3829
+ client: () => lazyManagementKeys({ apiBaseUrl: input.apiBaseUrl })
3830
+ },
3831
+ ...input.tokenStore === void 0 ? {} : { tokenStore: input.tokenStore }
3832
+ });
3833
+
3834
+ // src/v2-entry/connect-copy/connect-copy.ts
3835
+ var CONNECT_COPY = {
3836
+ failFastClientId: "This Route 33 build has no OAuth client id. Reinstall it from the opencode-dist ref, or set ROUTE33_CLIENT_ID and restart OpenCode.",
3837
+ failFastWorktree: "Route 33 attributes usage to a project inside a Git worktree. Open a Git repository (or run git init), then connect again.",
3838
+ selectionInvalid: "That Route 33 option is no longer available. Connect again and pick another.",
3839
+ signedInNextStep: "Signed in to Route 33 \u2713. One more step: connect again, choose Route 33, and assign a budget for this folder."
3840
+ };
3841
+
3842
+ // src/v2-entry/parse-connect-answer/parse-connect-answer.ts
3843
+ var ATTRIBUTION_FIELD_KEY = "attribution";
3844
+ var parseConnectAnswer = (answer) => {
3845
+ const value = answer[ATTRIBUTION_FIELD_KEY];
3846
+ return typeof value === "string" && value.length > 0 ? value : void 0;
3847
+ };
3848
+
3849
+ // src/v2-entry/connect-authorize/connect-authorize.ts
3850
+ var createConnectAuthorize = (deps) => async (answer = {}) => {
3851
+ if (deps.clientId === "") {
3852
+ throw new GatewayCredentialError(CONNECT_COPY.failFastClientId);
3853
+ }
3854
+ if (detectWorktreeRoot(deps.directory) === void 0) {
3855
+ throw new GatewayCredentialError(CONNECT_COPY.failFastWorktree);
3856
+ }
3857
+ const selection = parseConnectAnswer(answer);
3858
+ if (selection !== void 0) {
3859
+ const choice = deps.getPlan()?.choicesBySelection[selection];
3860
+ if (choice === void 0) {
3861
+ throw new AttributionPromptError("ATTRIBUTION_SELECTION_INVALID");
3862
+ }
3863
+ writeAttribution(deps.state, choice);
3864
+ }
3865
+ if (readAttribution(deps.state) !== void 0) {
3866
+ const callback2 = deps.resolveCredential();
3867
+ const decision2 = await deps.stash.urlForConnect(
3868
+ callback2.then(() => void 0)
3869
+ );
3870
+ return {
3871
+ callback: callback2,
3872
+ instructions: decision2.instructions,
3873
+ mode: "auto",
3874
+ url: decision2.url
3875
+ };
3876
+ }
3877
+ const runtime = deps.getRuntime();
3878
+ if (runtime === void 0) {
3879
+ throw new GatewayCredentialError(
3880
+ "Route 33 authorization is unavailable in this host"
3881
+ );
3882
+ }
3883
+ const callback = (async () => {
3884
+ await runtime.login();
3885
+ try {
3886
+ return await deps.resolveCredential();
3887
+ } catch (error) {
3888
+ if (error instanceof AttributionPromptError && error.code === "ATTRIBUTION_PRESENTER_UNAVAILABLE") {
3889
+ await deps.registerProjectForm();
3890
+ throw new GatewayCredentialError(CONNECT_COPY.signedInNextStep);
3891
+ }
3892
+ throw error;
3893
+ }
3894
+ })();
3895
+ const decision = await deps.stash.urlForConnect(
3896
+ callback.catch(() => void 0)
3897
+ );
3898
+ return {
3899
+ callback,
3900
+ instructions: decision.instructions,
3901
+ mode: "auto",
3902
+ url: decision.url
3903
+ };
3904
+ };
3905
+
3906
+ // src/v2-entry/connect-form/connect-form.ts
3907
+ var ATTRIBUTION_FIELD_KEY2 = "attribution";
3908
+ var buildConnectForm = (plan) => ({
3909
+ key: ATTRIBUTION_FIELD_KEY2,
3910
+ options: plan.options.map((option) => ({
3911
+ label: option.label,
3912
+ value: option.selection
3913
+ })),
3914
+ required: true,
3915
+ title: "Which budget covers Route 33 usage in this folder?",
3916
+ type: "string"
3917
+ });
3918
+
3919
+ // src/v2-entry/map-route33-v2-provider/map-route33-v2-provider.ts
3920
+ import { Integration, Model, Provider } from "@opencode/plugin";
3921
+ var mapRoute33V2Provider = (provider) => {
3922
+ const providerID = Provider.ID.make("route_33");
3923
+ const info = Object.assign(Provider.Info.empty(providerID), {
3924
+ activation: "enabled",
3925
+ integrationID: Integration.ID.make("route33"),
3926
+ name: provider?.name ?? "route_33",
3927
+ package: "@opencode/ai/providers/openai-compatible"
3928
+ });
3929
+ if (provider === void 0) {
3930
+ return { info, models: [] };
3931
+ }
3932
+ const options = provider.options;
3933
+ const baseURL = options?.["baseURL"];
3934
+ const apiKey = options?.["apiKey"];
3935
+ const headerTimeout = options?.["headerTimeout"];
3936
+ return {
3937
+ info: Object.assign(info, {
3938
+ settings: {
3939
+ apiKey,
3940
+ baseURL,
3941
+ headerTimeout
3942
+ }
3943
+ }),
3944
+ models: Object.entries(provider.models ?? {}).flatMap(
3945
+ ([modelID, model]) => model === void 0 ? [] : [
3946
+ {
3947
+ ...Model.Info.default(providerID, Model.ID.make(modelID)),
3948
+ name: typeof model["name"] === "string" ? model["name"] : modelID
3949
+ }
3950
+ ]
3951
+ )
3952
+ };
3953
+ };
3954
+
3955
+ // src/v2-entry/direct-key-mode/direct-key-mode.ts
3956
+ var directKeyMode = async (context, options, compose) => {
3957
+ const hooks = await compose(options)({
3958
+ worktree: context.location.directory
3959
+ });
3960
+ const config = {};
3961
+ await hooks.config(config);
3962
+ const provider = mapRoute33V2Provider(
3963
+ config.provider?.route_33
3964
+ );
3965
+ const providerTransform = await context.provider.transform((editor) => {
3966
+ editor.add(provider);
3967
+ });
3968
+ await context.provider.reload();
3969
+ return async () => {
3970
+ await providerTransform.dispose();
3971
+ };
3972
+ };
3973
+
3974
+ // src/v2-entry/host-lease-state-store/host-lease-state-store.ts
3975
+ var hostLeaseStateStore = (options) => ({
3976
+ get: async () => {
3977
+ const value = await options.storage.get(options.key);
3978
+ if (value === void 0) {
3979
+ return void 0;
3980
+ }
3981
+ if (typeof value !== "string") {
3982
+ throw new ContractParseError("lease metadata must be a JSON string");
3983
+ }
3984
+ return LeaseState.restore(JSON.parse(value)).snapshot();
3985
+ },
3986
+ set: async (state) => {
3987
+ if (state === void 0) {
3988
+ await options.storage.remove(options.key);
3989
+ return;
3990
+ }
3991
+ await options.storage.set(options.key, JSON.stringify(state));
3992
+ }
3993
+ });
3994
+
3995
+ // src/v2-entry/host-token-store/host-token-store.ts
3996
+ var DEFAULT_TOKEN_KEY = "route33:auth";
3997
+ var hostTokenStore = (options) => ({
3998
+ get: async () => {
3999
+ const value = await options.storage.get(options.key ?? DEFAULT_TOKEN_KEY);
4000
+ return typeof value === "string" ? value : void 0;
4001
+ },
4002
+ set: async (value) => {
4003
+ if (value === void 0) {
4004
+ await options.storage.remove(options.key ?? DEFAULT_TOKEN_KEY);
4005
+ return;
4006
+ }
4007
+ await options.storage.set(options.key ?? DEFAULT_TOKEN_KEY, value);
4008
+ }
4009
+ });
4010
+
4011
+ // src/v2-entry/parse-r33-subcommand/parse-r33-subcommand.ts
4012
+ var SUBCOMMANDS = ["inspect", "rotate", "reset"];
4013
+ var parseR33Subcommand = (promptText) => {
4014
+ const [first = "", ...rest] = promptText.trim().split(/\s+/u).filter(Boolean);
4015
+ const name = first.replace(/^\/+/u, "").split(":")[0] ?? "";
4016
+ const argument = name === "r33" ? rest[0] : first;
4017
+ if (argument === void 0 || argument === "") {
4018
+ return "inspect";
4019
+ }
4020
+ return SUBCOMMANDS.includes(argument) ? argument : "help";
4021
+ };
4022
+
4023
+ // src/v2-entry/r33-command/r33-command.ts
4024
+ var executeR33Command = async (input) => {
4025
+ const subcommand = parseR33Subcommand(input.promptText);
4026
+ if (input.handlers === void 0) {
4027
+ return "Route 33 is not connected in this folder. Run /connect, choose Route 33, and follow the steps.";
4028
+ }
4029
+ if (subcommand === "help") {
4030
+ return "Route 33 usage: /r33 [inspect|rotate|reset] \u2014 inspect shows attribution and key rotation, rotate rotates the key now, reset clears the assigned budget (assign again with /connect).";
4031
+ }
4032
+ switch (subcommand) {
4033
+ case "rotate": {
4034
+ const lease = await input.handlers["rotate-now"]();
4035
+ return `Route 33 key rotated. Key ${lease.keyId} is valid until ${lease.expiresAt}.`;
4036
+ }
4037
+ case "reset": {
4038
+ await input.handlers["reset-attribution"]();
4039
+ return "Route 33 attribution cleared. Run /connect, choose Route 33, and assign a budget again.";
4040
+ }
4041
+ default: {
4042
+ return JSON.stringify(input.handlers["inspect-session"](), null, 2);
4043
+ }
4044
+ }
4045
+ };
4046
+
4047
+ // src/v2-entry/v2-unattributed-model-gate/v2-unattributed-model-gate.ts
4048
+ var GATE_COPY = "Route 33 needs a budget for this folder first \u2014 finish the setup dialog in the TUI, or run /connect, choose Route 33, and follow the steps.";
4049
+ var createV2UnattributedModelGate = (deps) => async (input) => {
4050
+ if (input.model.providerID !== "route_33") return;
4051
+ const session = await deps.session.get({ sessionID: input.sessionID });
4052
+ const directory = session?.location.directory ?? deps.fallbackDirectory;
4053
+ const state = {
4054
+ directory,
4055
+ outsideWorktree: "return-undefined"
4056
+ };
4057
+ if (readAttribution(state) !== void 0) return;
4058
+ throw new Error(GATE_COPY);
4059
+ };
4060
+
4061
+ // src/v2-entry/workspace-key-request-hook/workspace-key-request-hook.ts
4062
+ var createWorkspaceKeyHook = (deps) => async (input) => {
4063
+ const target = new URL(input.request.url);
4064
+ const gateway = new URL(deps.gatewayBase);
4065
+ if (target.origin !== gateway.origin) {
4066
+ return;
4067
+ }
4068
+ const session = await deps.session.get({ sessionID: input.sessionID });
4069
+ const key = await deps.resolveKey(session.location.directory);
4070
+ if (key === void 0) {
4071
+ return;
4072
+ }
4073
+ input.request.headers.set("Authorization", `Bearer ${key.value}`);
4074
+ };
4075
+
4076
+ // src/v2-entry/create-route33-v2-plugin/create-route33-v2-plugin.ts
4077
+ var createRoute33V2Plugin = (options = {}) => {
4078
+ const clientId = options.clientId ?? readEnvClientId() ?? DEFAULT_ROUTE33_CLIENT_ID;
4079
+ const issuerUrl = options.issuerUrl ?? readEnvAuthIssuer() ?? DEFAULT_AUTH_ISSUER_URL;
4080
+ const apiBaseUrl = options.apiBaseUrl ?? readEnvApiBase() ?? DEFAULT_MANAGEMENT_API_BASE_URL;
4081
+ return Plugin.define({
4082
+ id: "route33",
4083
+ setup: async (context) => {
4084
+ const directKey = readEnvDirectApiKey();
4085
+ if (directKey !== void 0) {
4086
+ return directKeyMode(context, options, createRoute33Plugin);
4087
+ }
4088
+ const methodID = Integration2.MethodID.make("browser");
4089
+ const stash = createAuthorizationUrlStash();
4090
+ const state = {
4091
+ directory: context.location.directory,
4092
+ outsideWorktree: "return-undefined"
4093
+ };
4094
+ const installation = createLeaseInstallationFactory({
4095
+ createSecretStore: (scopeId) => new HostStorageSecretStore({
4096
+ key: `route33:lease:${scopeId}`,
4097
+ storage: context.storage
4098
+ }),
4099
+ createStateStore: (scopeId) => hostLeaseStateStore({
4100
+ key: `route33:lease:${scopeId}:state`,
4101
+ storage: context.storage
4102
+ })
4103
+ });
4104
+ let runtime;
4105
+ let provider = mapRoute33V2Provider(void 0);
4106
+ let connectField;
4107
+ let plan;
4108
+ const runtimeInput = {
4109
+ ...options.runtime ?? buildProductionRuntimeOptions({
4110
+ apiBaseUrl,
4111
+ browserRunner: stash.browserRunner,
4112
+ clientId,
4113
+ issuerUrl,
4114
+ tokenStore: hostTokenStore({ storage: context.storage })
4115
+ }),
4116
+ installation
4117
+ };
4118
+ const hooks = await createRoute33Plugin({
4119
+ ...options,
4120
+ bindRuntime: (instance) => {
4121
+ runtime = instance;
4122
+ },
4123
+ clientId,
4124
+ runtime: runtimeInput
4125
+ })({ worktree: context.location.directory });
4126
+ const resolveCredential = async () => {
4127
+ const config = {};
4128
+ await hooks.config(config);
4129
+ provider = mapRoute33V2Provider(
4130
+ config.provider?.route_33
4131
+ );
4132
+ const stored = await runtime?.storedReusableKey();
4133
+ if (stored === void 0) {
4134
+ throw new GatewayCredentialError(
4135
+ "Route 33 authorization completed without a usable gateway lease"
4136
+ );
4137
+ }
4138
+ await context.provider.reload();
4139
+ return {
4140
+ access: stored.key.value,
4141
+ expires: Date.parse(stored.lease.expiresAt),
4142
+ methodID,
4143
+ refresh: stored.lease.keyId,
4144
+ type: "oauth"
4145
+ };
4146
+ };
4147
+ const registerProjectForm = async () => {
4148
+ if (runtime === void 0 || plan !== void 0) {
4149
+ return;
4150
+ }
4151
+ plan = createAttributionPrompt(await runtime.listProjects());
4152
+ connectField = buildConnectForm(plan);
4153
+ await context.integration.reload();
4154
+ };
4155
+ const authorize = createConnectAuthorize({
4156
+ clientId,
4157
+ directory: context.location.directory,
4158
+ getPlan: () => plan,
4159
+ getRuntime: () => runtime,
4160
+ registerProjectForm,
4161
+ resolveCredential,
4162
+ stash,
4163
+ state
4164
+ });
4165
+ const providerTransform = await context.provider.transform((editor) => {
4166
+ editor.add(provider);
4167
+ });
4168
+ const integrationTransform = await context.integration.transform(
4169
+ (editor) => {
4170
+ editor.method.update({
4171
+ authorize: (answer) => authorize(answer),
4172
+ integrationID: "route33",
4173
+ method: {
4174
+ id: methodID,
4175
+ label: TRUSTED_PARTY_COPY.title,
4176
+ type: "oauth",
4177
+ ...connectField === void 0 ? {} : { form: [connectField] }
4178
+ },
4179
+ refresh: () => resolveCredential()
4180
+ });
4181
+ }
4182
+ );
4183
+ const commandTransform = await context.command.transform((editor) => {
4184
+ editor.add({
4185
+ description: "Route 33 status and key rotation: /r33 [inspect|rotate|reset]",
4186
+ execute: async ({ prompt, sessionID }) => {
4187
+ const text = await executeR33Command({
4188
+ handlers: runtime?.commandHandlers(),
4189
+ promptText: prompt.text
4190
+ });
4191
+ await context.session.synthetic({
4192
+ delivery: "queue",
4193
+ sessionID,
4194
+ text
4195
+ });
4196
+ },
4197
+ name: "r33"
4198
+ });
4199
+ });
4200
+ const sessionHook = await context.session.hook(
4201
+ "http.request",
4202
+ createWorkspaceKeyHook({
4203
+ gatewayBase: readEnvBaseUrl() ?? DEFAULT_BASE_URL,
4204
+ resolveKey: (directory) => {
4205
+ const active = runtime;
4206
+ return active === void 0 ? Promise.resolve(void 0) : active.resolveWorkspaceCredential(directory);
4207
+ },
4208
+ session: context.session
4209
+ })
4210
+ );
4211
+ const modelGate = await context.session.hook(
4212
+ "model.request",
4213
+ createV2UnattributedModelGate({
4214
+ fallbackDirectory: context.location.directory,
4215
+ session: context.session
4216
+ }),
4217
+ { providerID: "route_33" }
4218
+ );
4219
+ const bootstrapFromRefresh = async () => {
4220
+ if (runtime === void 0) {
4221
+ return;
4222
+ }
4223
+ try {
4224
+ await runtime.login({ allowBrowser: false });
4225
+ } catch {
4226
+ return;
4227
+ }
4228
+ await registerProjectForm().catch(() => void 0);
4229
+ try {
4230
+ const config = {};
4231
+ await hooks.config(config);
4232
+ provider = mapRoute33V2Provider(
4233
+ config.provider?.route_33
4234
+ );
4235
+ } catch (error) {
4236
+ logger.info(
4237
+ `route_33: catalog bootstrap skipped: ${error instanceof Error ? error.message : String(error)}`
4238
+ );
4239
+ }
4240
+ };
4241
+ await bootstrapFromRefresh();
4242
+ await Promise.all([
4243
+ context.integration.reload(),
4244
+ context.provider.reload()
4245
+ ]);
4246
+ return async () => {
4247
+ await Promise.all([
4248
+ commandTransform.dispose(),
4249
+ integrationTransform.dispose(),
4250
+ providerTransform.dispose(),
4251
+ sessionHook.dispose(),
4252
+ modelGate.dispose()
4253
+ ]);
4254
+ };
4255
+ }
4256
+ });
4257
+ };
4258
+
4259
+ // src/v2-entry/index.ts
4260
+ var Route33PluginV2 = createRoute33V2Plugin();
4261
+ var v2_entry_default = Route33PluginV2;
4262
+ export {
4263
+ Route33PluginV2,
4264
+ createRoute33V2Plugin,
4265
+ v2_entry_default as default
4266
+ };