@jacobbd/relay-ai 0.2.3 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -76,7 +76,7 @@ function classifyModelFormat(modelId, providerNpm) {
76
76
  if (lower.startsWith("gemini-")) return "unsupported";
77
77
  return "openai";
78
78
  }
79
- var VERSION = "0.2.3";
79
+ var VERSION = "0.2.6";
80
80
 
81
81
  // src/provider-factory.ts
82
82
  var RESPONSES_ONLY_PREFIXES = [
@@ -196,11 +196,15 @@ var OPENAI_EFFORT_LEVELS = ["low", "medium", "high", "xhigh"];
196
196
  var GEMINI_EFFORT_LEVELS = ["low", "medium", "high"];
197
197
  var MISTRAL_EFFORT_LEVELS = ["high", "off"];
198
198
  var XAI_EFFORT_LEVELS = ["none", "low", "medium", "high"];
199
+ var OPENROUTER_EFFORT_LEVELS = ["none", "minimal", "low", "medium", "high", "xhigh"];
199
200
  var DEEPSEEK_EFFORT_LEVELS = ["high", "max", "off"];
200
201
  var EMPTY_REASONING = {
201
202
  levels: [],
202
203
  defaultLevel: "",
203
- supportsSummaries: false
204
+ supportsSummaries: false,
205
+ mode: "none",
206
+ source: "none",
207
+ confidence: "inferred"
204
208
  };
205
209
  var EFFORT_DESCRIPTIONS = {
206
210
  off: "Turn off extended reasoning",
@@ -257,6 +261,41 @@ function isDeepSeekReasoningModel(modelId) {
257
261
  const lower = modelId.toLowerCase();
258
262
  return lower === "deepseek-v4-flash" || lower === "deepseek-v4-pro" || lower.startsWith("deepseek-v4-flash-") || lower.startsWith("deepseek-v4-pro-") || lower === "deepseek-reasoner" || lower === "deepseek-chat";
259
263
  }
264
+ function hasSupportedParameter(metadata, param) {
265
+ return (metadata?.supportedParameters ?? []).some((p19) => p19 === param);
266
+ }
267
+ function isOpenRouterRoute(npm, metadata) {
268
+ return npm === "@openrouter/ai-sdk-provider" || metadata?.providerId === "openrouter" || metadata?.apiBaseUrl?.includes("openrouter.ai") === true;
269
+ }
270
+ function openRouterReasoningCapabilities(metadata) {
271
+ if (metadata?.supportedParameters && !hasSupportedParameter(metadata, "reasoning")) {
272
+ return {
273
+ ...EMPTY_REASONING,
274
+ source: "provider-metadata",
275
+ confidence: "documented"
276
+ };
277
+ }
278
+ if (hasSupportedParameter(metadata, "reasoning")) {
279
+ return {
280
+ levels: [...OPENROUTER_EFFORT_LEVELS],
281
+ defaultLevel: "medium",
282
+ supportsSummaries: false,
283
+ mode: "controllable",
284
+ source: "provider-metadata",
285
+ confidence: "documented",
286
+ wireFormat: { kind: "openrouter-reasoning" }
287
+ };
288
+ }
289
+ if (metadata?.reasoning) {
290
+ return {
291
+ ...EMPTY_REASONING,
292
+ mode: "internal-only",
293
+ source: "model-metadata",
294
+ confidence: "inferred"
295
+ };
296
+ }
297
+ return EMPTY_REASONING;
298
+ }
260
299
  function mapCodexEffortToDeepSeek(effort) {
261
300
  switch (effort) {
262
301
  case "off":
@@ -356,40 +395,81 @@ function mapCodexEffortToGeminiBudget(effort) {
356
395
  if (!level) return void 0;
357
396
  return GEMINI_25_BUDGETS[level];
358
397
  }
359
- function getReasoningCapabilities(npm, modelId) {
398
+ function getReasoningCapabilities(npm, modelId, metadata) {
360
399
  const id = modelId.toLowerCase();
400
+ if (isOpenRouterRoute(npm, metadata)) {
401
+ return openRouterReasoningCapabilities(metadata);
402
+ }
361
403
  if (npm === "@ai-sdk/anthropic" || id.startsWith("claude-")) {
362
- if (isClaudeReasoningModel(modelId)) {
363
- return { levels: [...ANTHROPIC_EFFORT_LEVELS], defaultLevel: "high", supportsSummaries: true };
404
+ const isClaude = isClaudeReasoningModel(modelId);
405
+ if (isClaude || metadata?.reasoning) {
406
+ return {
407
+ levels: [...ANTHROPIC_EFFORT_LEVELS],
408
+ defaultLevel: "high",
409
+ supportsSummaries: true,
410
+ mode: "controllable",
411
+ source: isClaude ? "provider-rule" : "model-metadata",
412
+ confidence: isClaude ? "documented" : "inferred",
413
+ wireFormat: { kind: "anthropic-thinking" }
414
+ };
364
415
  }
365
416
  return EMPTY_REASONING;
366
417
  }
367
418
  if (npm === "@ai-sdk/openai" || npm === "@ai-sdk/azure") {
368
- if (modelPrefersResponsesApi(modelId)) {
419
+ const prefersResponses = modelPrefersResponsesApi(modelId);
420
+ if (prefersResponses || metadata?.reasoning) {
369
421
  return {
370
422
  levels: [...OPENAI_EFFORT_LEVELS],
371
423
  defaultLevel: "medium",
372
- supportsSummaries: true
424
+ supportsSummaries: true,
425
+ mode: "controllable",
426
+ source: prefersResponses ? "provider-rule" : "model-metadata",
427
+ confidence: prefersResponses ? "documented" : "inferred",
428
+ wireFormat: { kind: "openai-reasoning-effort" }
373
429
  };
374
430
  }
375
431
  return EMPTY_REASONING;
376
432
  }
377
433
  if (npm === "@ai-sdk/google" || id.startsWith("gemini-")) {
378
434
  if (isGeminiReasoningModel(modelId)) {
379
- return { levels: [...GEMINI_EFFORT_LEVELS], defaultLevel: "medium", supportsSummaries: true };
435
+ return {
436
+ levels: [...GEMINI_EFFORT_LEVELS],
437
+ defaultLevel: "medium",
438
+ supportsSummaries: true,
439
+ mode: "controllable",
440
+ source: "provider-rule",
441
+ confidence: "documented",
442
+ wireFormat: { kind: "google-thinking-config" }
443
+ };
380
444
  }
381
445
  return EMPTY_REASONING;
382
446
  }
383
447
  if (npm === "@ai-sdk/mistral") {
384
448
  if (isMistralReasoningModel(modelId)) {
385
- return { levels: [...MISTRAL_EFFORT_LEVELS], defaultLevel: "high", supportsSummaries: false };
449
+ return {
450
+ levels: [...MISTRAL_EFFORT_LEVELS],
451
+ defaultLevel: "high",
452
+ supportsSummaries: false,
453
+ mode: "controllable",
454
+ source: "provider-rule",
455
+ confidence: "documented",
456
+ wireFormat: { kind: "mistral-reasoning-effort" }
457
+ };
386
458
  }
387
459
  return EMPTY_REASONING;
388
460
  }
389
461
  if (npm === "@ai-sdk/xai") {
390
462
  if (isXaiReasoningEffortModel(modelId)) {
391
463
  const levels = modelPrefersResponsesApi(modelId) ? ["low", "medium", "high", "xhigh"] : [...XAI_EFFORT_LEVELS];
392
- return { levels, defaultLevel: "low", supportsSummaries: true };
464
+ return {
465
+ levels,
466
+ defaultLevel: "low",
467
+ supportsSummaries: true,
468
+ mode: "controllable",
469
+ source: "provider-rule",
470
+ confidence: "documented",
471
+ wireFormat: { kind: "openai-reasoning-effort" }
472
+ };
393
473
  }
394
474
  return EMPTY_REASONING;
395
475
  }
@@ -397,7 +477,44 @@ function getReasoningCapabilities(npm, modelId) {
397
477
  return {
398
478
  levels: [...DEEPSEEK_EFFORT_LEVELS],
399
479
  defaultLevel: "high",
400
- supportsSummaries: true
480
+ supportsSummaries: true,
481
+ mode: "controllable",
482
+ source: "provider-rule",
483
+ confidence: "documented",
484
+ wireFormat: { kind: "deepseek-thinking" }
485
+ };
486
+ }
487
+ if (hasSupportedParameter(metadata, "reasoning_effort")) {
488
+ return {
489
+ levels: ["low", "medium", "high", "xhigh"],
490
+ defaultLevel: "medium",
491
+ supportsSummaries: false,
492
+ mode: "controllable",
493
+ source: "provider-metadata",
494
+ confidence: "documented",
495
+ wireFormat: { kind: "openai-reasoning-effort" }
496
+ };
497
+ }
498
+ if (hasSupportedParameter(metadata, "reasoning")) {
499
+ return {
500
+ levels: [...OPENROUTER_EFFORT_LEVELS],
501
+ defaultLevel: "medium",
502
+ supportsSummaries: false,
503
+ mode: "controllable",
504
+ source: "provider-metadata",
505
+ confidence: "documented",
506
+ wireFormat: { kind: "openrouter-reasoning" }
507
+ };
508
+ }
509
+ if (metadata?.reasoning) {
510
+ return {
511
+ levels: ["low", "medium", "high"],
512
+ defaultLevel: "medium",
513
+ supportsSummaries: false,
514
+ mode: "controllable",
515
+ source: "model-metadata",
516
+ confidence: "inferred",
517
+ wireFormat: { kind: "openai-reasoning-effort" }
401
518
  };
402
519
  }
403
520
  return EMPTY_REASONING;
@@ -408,8 +525,15 @@ function buildCodexReasoningLevels(capabilities) {
408
525
  description: EFFORT_DESCRIPTIONS[effort] ?? effort
409
526
  }));
410
527
  }
411
- function effortProviderOptions(npm, effort, modelId) {
528
+ function effortProviderOptions(npm, effort, modelId, metadata) {
412
529
  if (!effort) return void 0;
530
+ if (isOpenRouterRoute(npm, metadata)) {
531
+ const caps = openRouterReasoningCapabilities(metadata);
532
+ if (caps.mode !== "controllable") return void 0;
533
+ const allowed = new Set(OPENROUTER_EFFORT_LEVELS);
534
+ const mapped = allowed.has(effort) ? effort : effort === "max" ? "xhigh" : void 0;
535
+ return mapped ? { openrouter: { reasoning: { effort: mapped, exclude: false } } } : void 0;
536
+ }
413
537
  if (npm === "@ai-sdk/openai" || npm === "@ai-sdk/azure") {
414
538
  if (!modelId || !modelPrefersResponsesApi(modelId)) return void 0;
415
539
  const reasoningEffort = mapCodexEffortToOpenAI(effort);
@@ -420,13 +544,14 @@ function effortProviderOptions(npm, effort, modelId) {
420
544
  const reasoningEffort = mapCodexEffortToXai(effort);
421
545
  return reasoningEffort ? { xai: { reasoningEffort } } : void 0;
422
546
  }
423
- if (npm === "@ai-sdk/anthropic") {
547
+ if (npm === "@ai-sdk/anthropic" || npm === VERTEX_ANTHROPIC_NPM) {
548
+ if (!modelId || !isClaudeReasoningModel(modelId)) return void 0;
424
549
  const mapped = mapCodexEffortToAnthropic(effort);
425
550
  return mapped ? { anthropic: { thinking: { type: "adaptive", effort: mapped } } } : void 0;
426
551
  }
427
552
  if (npm === "@ai-sdk/google") {
428
- const id2 = modelId ?? "";
429
- if (isGemini3Model(id2)) {
553
+ const id = modelId ?? "";
554
+ if (isGemini3Model(id)) {
430
555
  const thinkingLevel = mapCodexEffortToGeminiLevel(effort);
431
556
  return thinkingLevel ? { google: { thinkingConfig: { thinkingLevel, includeThoughts: true } } } : void 0;
432
557
  }
@@ -438,9 +563,21 @@ function effortProviderOptions(npm, effort, modelId) {
438
563
  const reasoningEffort = effort === "off" || effort === "none" ? "none" : "high";
439
564
  return { mistral: { reasoningEffort } };
440
565
  }
441
- const id = (modelId ?? "").toLowerCase();
442
- if (isDeepSeekReasoningModel(id)) {
443
- return deepSeekEffortProviderOptions(effort);
566
+ if (npm === "@ai-sdk/openai-compatible" || npm === "@ai-sdk/openai") {
567
+ if (!modelId) return void 0;
568
+ if (isDeepSeekReasoningModel(modelId)) {
569
+ return deepSeekEffortProviderOptions(effort);
570
+ }
571
+ if (hasSupportedParameter(metadata, "reasoning_effort")) {
572
+ const reasoningEffort = mapCodexEffortToOpenAI(effort);
573
+ return reasoningEffort ? { openai: { reasoningEffort }, openaiCompatible: { reasoningEffort }, "openai-compatible": { reasoningEffort } } : void 0;
574
+ }
575
+ if (hasSupportedParameter(metadata, "reasoning")) {
576
+ const allowed = new Set(OPENROUTER_EFFORT_LEVELS);
577
+ const mapped = allowed.has(effort) ? effort : effort === "max" ? "xhigh" : void 0;
578
+ return mapped ? { openrouter: { reasoning: { effort: mapped, exclude: false } } } : void 0;
579
+ }
580
+ return void 0;
444
581
  }
445
582
  return void 0;
446
583
  }
@@ -1073,6 +1210,32 @@ import { dirname } from "path";
1073
1210
  // src/registry/types.ts
1074
1211
  var REGISTRY_SCHEMA_VERSION = 1;
1075
1212
 
1213
+ // src/registry/migrate.ts
1214
+ var LEGACY_CLOUD_PROVIDER_IDS = [
1215
+ { legacyId: "opencode", id: "zen", name: "OpenCode Zen" },
1216
+ { legacyId: "opencode-go", id: "go", name: "OpenCode Go" }
1217
+ ];
1218
+ function migrateLegacyCloudProviders(registry) {
1219
+ let changed = false;
1220
+ for (const { legacyId, id, name } of LEGACY_CLOUD_PROVIDER_IDS) {
1221
+ const legacyIdx = registry.providers.findIndex((provider) => provider.id === legacyId);
1222
+ if (legacyIdx < 0) continue;
1223
+ if (registry.providers.some((provider) => provider.id === id)) {
1224
+ registry.providers.splice(legacyIdx, 1);
1225
+ } else {
1226
+ registry.providers[legacyIdx] = {
1227
+ ...registry.providers[legacyIdx],
1228
+ id,
1229
+ templateId: id,
1230
+ name,
1231
+ api: {}
1232
+ };
1233
+ }
1234
+ changed = true;
1235
+ }
1236
+ return changed;
1237
+ }
1238
+
1076
1239
  // src/registry/validate.ts
1077
1240
  var PROVIDER_ID_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
1078
1241
  function isValidProviderId(id) {
@@ -1178,7 +1341,14 @@ function loadRegistry(path = getProvidersPath()) {
1178
1341
  }
1179
1342
  try {
1180
1343
  const raw = JSON.parse(readFileSync(path, "utf8"));
1181
- return parseRegistry(raw);
1344
+ const registry = parseRegistry(raw);
1345
+ if (migrateLegacyCloudProviders(registry)) {
1346
+ try {
1347
+ saveRegistry(registry, path);
1348
+ } catch {
1349
+ }
1350
+ }
1351
+ return registry;
1182
1352
  } catch {
1183
1353
  return { schemaVersion: REGISTRY_SCHEMA_VERSION, providers: [] };
1184
1354
  }
@@ -1576,14 +1746,19 @@ function resolveBaseURL(model, provider) {
1576
1746
  }
1577
1747
  function resolveCodexRoute(provider, model, apiKey) {
1578
1748
  const upstreamModelId2 = model.upstreamModelId || model.id;
1749
+ const inferredNpm = model.modelFormat === "anthropic" ? "@ai-sdk/anthropic" : "@ai-sdk/openai-compatible";
1750
+ const isZenGo = provider.id === "zen" || provider.id === "go";
1579
1751
  const base = {
1580
- npm: model.npm ?? (model.modelFormat === "anthropic" ? "@ai-sdk/anthropic" : "@ai-sdk/openai-compatible"),
1752
+ npm: isZenGo ? inferredNpm : model.npm ?? inferredNpm,
1581
1753
  baseURL: resolveBaseURL(model, provider),
1582
1754
  upstreamModelId: upstreamModelId2,
1583
1755
  apiKey,
1584
1756
  contextWindow: model.contextWindow,
1585
1757
  modelId: model.id,
1586
- providerId: provider.id
1758
+ providerId: provider.id,
1759
+ supportedParameters: model.supportedParameters,
1760
+ reasoning: model.reasoning,
1761
+ interleavedReasoningField: model.interleavedReasoningField
1587
1762
  };
1588
1763
  if (provider.id === "openai" && model.modelFormat === "openai") {
1589
1764
  return { tier: "direct", ...base };
@@ -1607,7 +1782,10 @@ function buildCodexProxyRoutesForProvider(provider, apiKey, selectedModelId, age
1607
1782
  apiKey: route.apiKey,
1608
1783
  baseURL: route.baseURL,
1609
1784
  upstreamModelId: route.upstreamModelId,
1610
- providerId: route.providerId
1785
+ providerId: route.providerId,
1786
+ supportedParameters: route.supportedParameters,
1787
+ reasoning: route.reasoning,
1788
+ interleavedReasoningField: route.interleavedReasoningField
1611
1789
  };
1612
1790
  });
1613
1791
  }
@@ -1665,8 +1843,8 @@ function buildCodexAppRootConfig(spec) {
1665
1843
  // src/codex/catalog.ts
1666
1844
  var DEFAULT_CONTEXT = 128e3;
1667
1845
  var CODEX_NO_REASONING_EFFORT = "none";
1668
- function codexCatalogReasoningFields(npm, wireId) {
1669
- const reasoning = getReasoningCapabilities(npm, wireId);
1846
+ function codexCatalogReasoningFields(npm, wireId, metadata) {
1847
+ const reasoning = getReasoningCapabilities(npm, wireId, metadata);
1670
1848
  if (reasoning.levels.length > 0) {
1671
1849
  return {
1672
1850
  supported_reasoning_levels: buildCodexReasoningLevels(reasoning),
@@ -1677,9 +1855,7 @@ function codexCatalogReasoningFields(npm, wireId) {
1677
1855
  }
1678
1856
  return {
1679
1857
  supported_reasoning_levels: buildCodexReasoningLevels({
1680
- levels: [CODEX_NO_REASONING_EFFORT],
1681
- defaultLevel: CODEX_NO_REASONING_EFFORT,
1682
- supportsSummaries: false
1858
+ levels: [CODEX_NO_REASONING_EFFORT]
1683
1859
  }),
1684
1860
  default_reasoning_level: CODEX_NO_REASONING_EFFORT,
1685
1861
  supports_reasoning_summaries: false,
@@ -1709,7 +1885,12 @@ function catalogEntryFromModel(model, providerName, priority, appCatalog = false
1709
1885
  const context = model.contextWindow ?? DEFAULT_CONTEXT;
1710
1886
  const label = formatCodexModelLabel(model);
1711
1887
  const wireId = model.upstreamModelId ?? model.id;
1712
- const reasoningFields = codexCatalogReasoningFields(model.npm ?? "", wireId);
1888
+ const reasoningFields = codexCatalogReasoningFields(model.npm ?? "", wireId, {
1889
+ apiBaseUrl: model.apiBaseUrl,
1890
+ supportedParameters: model.supportedParameters,
1891
+ reasoning: model.reasoning,
1892
+ interleavedReasoningField: model.interleavedReasoningField
1893
+ });
1713
1894
  return {
1714
1895
  slug,
1715
1896
  display_name: label,
@@ -2984,7 +3165,10 @@ function normalizeProviders(raw, opts) {
2984
3165
  npm: model.api?.npm,
2985
3166
  apiBaseUrl: model.api?.url,
2986
3167
  cost: model.cost,
2987
- contextWindow: resolveContextWindow(model.id, model.limit?.context)
3168
+ contextWindow: resolveContextWindow(model.id, model.limit?.context),
3169
+ supportedParameters: model.supportedParameters ?? model.supported_parameters,
3170
+ reasoning: model.reasoning,
3171
+ interleavedReasoningField: model.interleaved?.field
2988
3172
  });
2989
3173
  }
2990
3174
  if (models.length === 0) continue;
@@ -3123,7 +3307,10 @@ function modelToCached(model) {
3123
3307
  cost: model.cost,
3124
3308
  modelFormat: model.modelFormat,
3125
3309
  npm: model.npm,
3126
- apiUrl: model.apiBaseUrl
3310
+ apiUrl: model.apiBaseUrl,
3311
+ supportedParameters: model.supportedParameters,
3312
+ reasoning: model.reasoning,
3313
+ interleavedReasoningField: model.interleavedReasoningField
3127
3314
  };
3128
3315
  }
3129
3316
  function localProviderToRegistry(provider, opts) {
@@ -3144,7 +3331,6 @@ function localProviderToRegistry(provider, opts) {
3144
3331
  ...apiUrl ? { url: apiUrl } : {}
3145
3332
  },
3146
3333
  addedAt: (/* @__PURE__ */ new Date()).toISOString(),
3147
- refreshedAt: (/* @__PURE__ */ new Date()).toISOString(),
3148
3334
  modelsCache: {
3149
3335
  fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
3150
3336
  models: provider.models.map(modelToCached)
@@ -3206,11 +3392,25 @@ async function resolveRefreshCredential(provider, resolveKey) {
3206
3392
  function oauthAuthRef(providerId) {
3207
3393
  return `keyring:oauth:provider:${providerId}`;
3208
3394
  }
3395
+ function normalizeImportProviderIdentity(provider) {
3396
+ if (provider.id === "opencode") {
3397
+ return { ...provider, id: "zen", name: "OpenCode Zen" };
3398
+ }
3399
+ if (provider.id === "opencode-go") {
3400
+ return { ...provider, id: "go", name: "OpenCode Go" };
3401
+ }
3402
+ return provider;
3403
+ }
3209
3404
  function buildImportProviderList(raw, authEntries) {
3210
3405
  const oauthByProviderId = /* @__PURE__ */ new Map();
3211
- const apiProviders = normalizeProviders(raw);
3212
- const covered = new Set(apiProviders.map((p19) => p19.id));
3213
- const merged = [...apiProviders];
3406
+ const covered = /* @__PURE__ */ new Set();
3407
+ const merged = [];
3408
+ for (const provider of normalizeProviders(raw)) {
3409
+ const normalized = normalizeImportProviderIdentity(provider);
3410
+ if (covered.has(normalized.id)) continue;
3411
+ merged.push(normalized);
3412
+ covered.add(normalized.id);
3413
+ }
3214
3414
  for (const provider of raw) {
3215
3415
  if (provider.id === "opencode" || provider.id === "opencode-go") continue;
3216
3416
  if (covered.has(provider.id)) continue;
@@ -3293,7 +3493,8 @@ function parseModelList(body, npm) {
3293
3493
  brand: deriveBrand(family),
3294
3494
  contextWindow: resolveContextWindow(id),
3295
3495
  modelFormat: format,
3296
- npm
3496
+ npm,
3497
+ supportedParameters: Array.isArray(row.supported_parameters) ? row.supported_parameters : void 0
3297
3498
  });
3298
3499
  }
3299
3500
  return models;
@@ -3818,6 +4019,35 @@ var PROVIDER_TEMPLATES = [
3818
4019
  supported: false,
3819
4020
  unsupportedReason: "Uses gcloud Application Default Credentials \u2014 not supported via API key import."
3820
4021
  },
4022
+ {
4023
+ id: "opencode-cloud",
4024
+ name: "OpenCode Zen / Go",
4025
+ authType: "api",
4026
+ npm: "@ai-sdk/openai-compatible",
4027
+ signupUrl: "https://opencode.ai/auth",
4028
+ modelSource: "zen-go-api",
4029
+ supported: true
4030
+ },
4031
+ {
4032
+ id: "zen",
4033
+ name: "OpenCode Zen",
4034
+ authType: "api",
4035
+ npm: "@ai-sdk/openai-compatible",
4036
+ signupUrl: "https://opencode.ai/auth",
4037
+ modelSource: "zen-go-api",
4038
+ supported: true,
4039
+ addable: false
4040
+ },
4041
+ {
4042
+ id: "go",
4043
+ name: "OpenCode Go",
4044
+ authType: "api",
4045
+ npm: "@ai-sdk/openai-compatible",
4046
+ signupUrl: "https://opencode.ai/auth",
4047
+ modelSource: "zen-go-api",
4048
+ supported: true,
4049
+ addable: false
4050
+ },
3821
4051
  // OAuth-gated subscription providers — use relay-ai providers auth <id> to sign in
3822
4052
  {
3823
4053
  id: "github-copilot",
@@ -3831,11 +4061,16 @@ var PROVIDER_TEMPLATES = [
3831
4061
  }
3832
4062
  ];
3833
4063
  function listSupportedTemplates() {
3834
- return PROVIDER_TEMPLATES.filter((t) => t.supported && t.authType === "api");
4064
+ return PROVIDER_TEMPLATES.filter((t) => t.supported && t.authType === "api" && t.addable !== false);
3835
4065
  }
3836
4066
  function listAddableTemplates(configuredIds = []) {
3837
4067
  const configured = new Set(configuredIds);
3838
- return listSupportedTemplates().filter((t) => !configured.has(t.id));
4068
+ return listSupportedTemplates().filter((t) => {
4069
+ if (t.id === "opencode-cloud") {
4070
+ return !configured.has("zen") && !configured.has("go");
4071
+ }
4072
+ return !configured.has(t.id);
4073
+ });
3839
4074
  }
3840
4075
  function getTemplateById(id) {
3841
4076
  return PROVIDER_TEMPLATES.find((t) => t.id === id);
@@ -3847,14 +4082,6 @@ function filterTemplates(templates, query) {
3847
4082
  (t) => t.id.toLowerCase().includes(q) || t.name.toLowerCase().includes(q) || t.npm.toLowerCase().includes(q)
3848
4083
  );
3849
4084
  }
3850
- function matchesOpenCodeCloudSearch(query) {
3851
- const q = query.trim().toLowerCase();
3852
- if (!q) return false;
3853
- if (q.includes("opencode") || q.includes("open code")) return true;
3854
- if (q === "zen" || q === "go") return true;
3855
- if (q.includes("zen ") || q.startsWith("zen/") || q.endsWith(" zen")) return true;
3856
- return false;
3857
- }
3858
4085
 
3859
4086
  // src/registry/resolve-template.ts
3860
4087
  var TEMPLATE_ID_ALIASES = {
@@ -3955,8 +4182,20 @@ async function validateImportKey(lp, entry) {
3955
4182
  }
3956
4183
  return reject("invalid-key", "No API base URL \u2014 cannot verify key.");
3957
4184
  }
4185
+ let safeBaseUrl = baseUrl;
4186
+ const configuredUrl = entry.api.url?.trim();
4187
+ const templateDefault = catalogTemplate?.defaultBaseUrl?.trim();
4188
+ if (configuredUrl && configuredUrl !== templateDefault) {
4189
+ const urlCheck = await validateCustomEndpointUrl(baseUrl, {
4190
+ allowInsecureLocal: catalogTemplate?.apiKeyOptional === true
4191
+ });
4192
+ if (!urlCheck.ok || !urlCheck.normalizedUrl) {
4193
+ return reject("invalid-key", `${urlCheck.error ?? "Invalid API base URL."} ${urlCheck.hint ?? ""}`.trim());
4194
+ }
4195
+ safeBaseUrl = urlCheck.normalizedUrl;
4196
+ }
3958
4197
  if (npm === "@ai-sdk/anthropic") {
3959
- const result2 = await fetchAnthropicModels(baseUrl, key);
4198
+ const result2 = await fetchAnthropicModels(safeBaseUrl, key);
3960
4199
  if (result2.error) {
3961
4200
  return reject(
3962
4201
  placeholder ? "placeholder-key" : "invalid-key",
@@ -3965,8 +4204,8 @@ async function validateImportKey(lp, entry) {
3965
4204
  }
3966
4205
  return { canImport: true };
3967
4206
  }
3968
- const template = catalogTemplate ?? syntheticTemplate(entry, baseUrl);
3969
- const result = await fetchTemplateModels(template, key, baseUrl);
4207
+ const template = catalogTemplate ?? syntheticTemplate(entry, safeBaseUrl);
4208
+ const result = await fetchTemplateModels(template, key, safeBaseUrl);
3970
4209
  if (result.error) {
3971
4210
  return reject(
3972
4211
  placeholder ? "placeholder-key" : "invalid-key",
@@ -4016,6 +4255,7 @@ async function importFromOpencode(options = {}) {
4016
4255
  const authEntries = authFile?.entries ?? {};
4017
4256
  const { providers: fetched, oauth } = buildImportProviderList(raw, authEntries);
4018
4257
  const registry = loadRegistry();
4258
+ migrateLegacyCloudProviders(registry);
4019
4259
  const imported = [];
4020
4260
  const skipped = [];
4021
4261
  const keysSkipped = [];
@@ -4076,6 +4316,11 @@ async function importFromOpencode(options = {}) {
4076
4316
  continue;
4077
4317
  }
4078
4318
  }
4319
+ const saved = isOAuth ? await saveOAuthKey(lp.id, oauth) : await saveProviderKey(lp);
4320
+ if (!saved) {
4321
+ skipped.push({ id: lp.id, name: lp.name, reason: "credential-save-failed" });
4322
+ continue;
4323
+ }
4079
4324
  if (existingIdx >= 0) {
4080
4325
  registry.providers[existingIdx] = { ...entry, addedAt: registry.providers[existingIdx].addedAt };
4081
4326
  } else {
@@ -4083,11 +4328,8 @@ async function importFromOpencode(options = {}) {
4083
4328
  }
4084
4329
  imported.push(entry);
4085
4330
  importedIds.add(lp.id);
4086
- const saved = isOAuth ? await saveOAuthKey(lp.id, oauth) : await saveProviderKey(lp);
4087
- if (saved) {
4088
- keysSaved += 1;
4089
- if (isOAuth) oauthImported += 1;
4090
- }
4331
+ keysSaved += 1;
4332
+ if (isOAuth) oauthImported += 1;
4091
4333
  }
4092
4334
  const alreadyReportedIds = new Set(skipped.map((s) => s.id));
4093
4335
  const registryProviderIds = new Set(registry.providers.map((p19) => p19.id));
@@ -4588,10 +4830,13 @@ function upstreamModelId(model) {
4588
4830
  const id = model.upstreamModelId ?? model.id;
4589
4831
  return id.replace(/\[1m\]$/i, "");
4590
4832
  }
4833
+ function isOpenAIChatCompletionsModel(model) {
4834
+ return model.modelFormat === "openai" && (!!model.completionsUrl || model.sourceBackend === "zen" || model.sourceBackend === "go");
4835
+ }
4591
4836
  function formatOpenAIModels(models) {
4592
4837
  return {
4593
4838
  object: "list",
4594
- data: models.map((model) => ({
4839
+ data: models.filter(isOpenAIChatCompletionsModel).map((model) => ({
4595
4840
  id: model.id,
4596
4841
  object: "model",
4597
4842
  created: CREATED_AT_UNIX,
@@ -4623,20 +4868,24 @@ function extractBearerToken(value) {
4623
4868
  }
4624
4869
 
4625
4870
  // src/upstream-forward.ts
4626
- function anthropicUpstreamHeaders(apiKey, stream = false) {
4871
+ function anthropicUpstreamHeaders(apiKey, stream = false, inboundBeta) {
4627
4872
  const key = sanitizeCredential(apiKey) ?? apiKey.trim();
4628
- return {
4873
+ const headers = {
4629
4874
  "Content-Type": "application/json",
4630
4875
  "anthropic-version": "2023-06-01",
4631
4876
  Authorization: `Bearer ${key}`,
4632
4877
  "x-api-key": key,
4633
4878
  ...stream ? { Accept: "text/event-stream" } : {}
4634
4879
  };
4880
+ if (inboundBeta) {
4881
+ headers["anthropic-beta"] = inboundBeta;
4882
+ }
4883
+ return headers;
4635
4884
  }
4636
- async function postJsonUpstream(url, body, apiKey) {
4885
+ async function postJsonUpstream(url, body, apiKey, inboundBeta) {
4637
4886
  const response = await fetch(url, {
4638
4887
  method: "POST",
4639
- headers: anthropicUpstreamHeaders(apiKey, false),
4888
+ headers: anthropicUpstreamHeaders(apiKey, false, inboundBeta),
4640
4889
  body: JSON.stringify(body)
4641
4890
  });
4642
4891
  const text5 = await response.text();
@@ -4656,12 +4905,12 @@ var UpstreamUnreachableError = class extends Error {
4656
4905
  this.name = "UpstreamUnreachableError";
4657
4906
  }
4658
4907
  };
4659
- async function relayAnthropicMessages(res, messagesUrl, body, apiKey, clientWantsStream) {
4908
+ async function relayAnthropicMessages(res, messagesUrl, body, apiKey, clientWantsStream, inboundBeta) {
4660
4909
  let upstreamRes;
4661
4910
  try {
4662
4911
  upstreamRes = await fetch(messagesUrl, {
4663
4912
  method: "POST",
4664
- headers: anthropicUpstreamHeaders(apiKey, clientWantsStream),
4913
+ headers: anthropicUpstreamHeaders(apiKey, clientWantsStream, inboundBeta),
4665
4914
  body: JSON.stringify(body)
4666
4915
  });
4667
4916
  } catch (err) {
@@ -4687,20 +4936,19 @@ async function relayAnthropicMessages(res, messagesUrl, body, apiKey, clientWant
4687
4936
  res.end(JSON.stringify({ type: "error", error: { type: "api_error", message: "Upstream returned empty response body" } }));
4688
4937
  return;
4689
4938
  }
4690
- let json;
4939
+ const text5 = await upstreamRes.text();
4691
4940
  try {
4692
- json = await upstreamRes.json();
4941
+ JSON.parse(text5);
4693
4942
  } catch {
4694
4943
  res.writeHead(502, { "Content-Type": "application/json" });
4695
4944
  res.end(JSON.stringify({ type: "error", error: { type: "api_error", message: "Upstream response was not valid JSON" } }));
4696
4945
  return;
4697
4946
  }
4698
- const payload = JSON.stringify(json);
4699
4947
  res.writeHead(200, {
4700
4948
  "Content-Type": "application/json",
4701
- "Content-Length": Buffer.byteLength(payload).toString()
4949
+ "Content-Length": Buffer.byteLength(text5).toString()
4702
4950
  });
4703
- res.end(payload);
4951
+ res.end(text5);
4704
4952
  }
4705
4953
 
4706
4954
  // src/sdk-adapter.ts
@@ -4717,7 +4965,7 @@ function silenceSdkWarnings() {
4717
4965
  sdkWarningsSilenced = true;
4718
4966
  globalThis.AI_SDK_LOG_WARNINGS = false;
4719
4967
  }
4720
- var TOOL_USE_SIG_SEP = "::ts::";
4968
+ var TOOL_USE_SIG_SEP = "__ts__";
4721
4969
  function parseToolArguments(value) {
4722
4970
  if (value === null || value === void 0) return {};
4723
4971
  if (typeof value === "object" && !Array.isArray(value)) return value;
@@ -4740,15 +4988,26 @@ data: ${JSON.stringify(data)}
4740
4988
  `;
4741
4989
  }
4742
4990
  function splitToolUseId(id) {
4743
- const sep = id.lastIndexOf(TOOL_USE_SIG_SEP);
4744
- if (sep === -1) return { rawId: id };
4745
- return {
4746
- rawId: id.slice(0, sep),
4747
- thoughtSignature: id.slice(sep + TOOL_USE_SIG_SEP.length)
4748
- };
4991
+ let sep = id.lastIndexOf(TOOL_USE_SIG_SEP);
4992
+ if (sep !== -1) {
4993
+ return {
4994
+ rawId: id.slice(0, sep),
4995
+ thoughtSignature: Buffer.from(id.slice(sep + TOOL_USE_SIG_SEP.length), "base64url").toString("utf8")
4996
+ };
4997
+ }
4998
+ sep = id.lastIndexOf("::ts::");
4999
+ if (sep !== -1) {
5000
+ return {
5001
+ rawId: id.slice(0, sep),
5002
+ thoughtSignature: id.slice(sep + 6)
5003
+ };
5004
+ }
5005
+ return { rawId: id };
4749
5006
  }
4750
5007
  function encodeToolUseId(rawId, thoughtSignature) {
4751
- return thoughtSignature ? `${rawId}${TOOL_USE_SIG_SEP}${thoughtSignature}` : rawId;
5008
+ if (!thoughtSignature) return rawId;
5009
+ const encoded = Buffer.from(thoughtSignature, "utf8").toString("base64url");
5010
+ return `${rawId}${TOOL_USE_SIG_SEP}${encoded}`;
4752
5011
  }
4753
5012
  function serializeToolResultContent(content) {
4754
5013
  return typeof content === "string" ? content : JSON.stringify(content);
@@ -4950,7 +5209,7 @@ function translateRequest(body, npm, options) {
4950
5209
  const effort = anthropicEffortFromRequest(body) ?? options?.defaultEffort;
4951
5210
  const providerOptions = deepMergeProviderOptions(
4952
5211
  thinkingProviderOptions(npm),
4953
- effortProviderOptions(npm, effort, body.model)
5212
+ effortProviderOptions(npm, effort, body.model, options?.reasoningMetadata)
4954
5213
  );
4955
5214
  return {
4956
5215
  system,
@@ -5133,7 +5392,7 @@ async function generateAnthropicResponse(model, params, modelId) {
5133
5392
  ...r.text ? [{ type: "text", text: r.text }] : [],
5134
5393
  ...r.toolCalls.map((tc) => ({
5135
5394
  type: "tool_use",
5136
- id: tc.toolCallId,
5395
+ id: encodeToolUseId(tc.toolCallId, grabRoundTripSignature(tc)),
5137
5396
  name: tc.toolName,
5138
5397
  input: tc.input
5139
5398
  }))
@@ -5259,11 +5518,13 @@ function startProxyCatalog(routes, defaultAliasId, debug = false) {
5259
5518
  return;
5260
5519
  }
5261
5520
  if (route.modelFormat === "anthropic") {
5521
+ const betaHeaderRaw = req.headers["anthropic-beta"];
5522
+ const inboundBeta = Array.isArray(betaHeaderRaw) ? betaHeaderRaw.join(",") : betaHeaderRaw;
5262
5523
  const forwardBody = { ...anthropicBody, model: route.realModelId };
5263
5524
  const targetUrl = `${upstreamUrl}/v1/messages`;
5264
5525
  plog(() => `anthropic-passthrough: model=${route.realModelId}, stream=${clientWantsStream}`);
5265
5526
  try {
5266
- await relayAnthropicMessages(res, targetUrl, forwardBody, apiKey, clientWantsStream);
5527
+ await relayAnthropicMessages(res, targetUrl, forwardBody, apiKey, clientWantsStream, inboundBeta);
5267
5528
  } catch (err) {
5268
5529
  const message = err instanceof UpstreamUnreachableError ? err.message : String(err);
5269
5530
  plog(() => `anthropic-passthrough error: ${message}`);
@@ -5272,7 +5533,15 @@ function startProxyCatalog(routes, defaultAliasId, debug = false) {
5272
5533
  return;
5273
5534
  }
5274
5535
  if (isSdkMigratedNpm(route.npm)) {
5275
- const params = translateRequest(anthropicBody, route.npm);
5536
+ const params = translateRequest(anthropicBody, route.npm, {
5537
+ reasoningMetadata: {
5538
+ providerId: route.providerId,
5539
+ apiBaseUrl: route.baseURL,
5540
+ supportedParameters: route.supportedParameters,
5541
+ reasoning: route.reasoning,
5542
+ interleavedReasoningField: route.interleavedReasoningField
5543
+ }
5544
+ });
5276
5545
  plog(
5277
5546
  () => `sdk: npm=${route.npm} model=${route.realModelId}, stream=${clientWantsStream}, tools=${anthropicBody.tools?.length ?? 0}, msgs=${params.messages.length}`
5278
5547
  );
@@ -5349,7 +5618,11 @@ function startProxy(completionsUrl, modelId, debug = false, contextWindow, sdk)
5349
5618
  modelFormat: "openai",
5350
5619
  contextWindow,
5351
5620
  npm: sdk?.npm,
5352
- baseURL: sdk?.baseURL
5621
+ baseURL: sdk?.baseURL,
5622
+ providerId: sdk?.providerId,
5623
+ supportedParameters: sdk?.supportedParameters,
5624
+ reasoning: sdk?.reasoning,
5625
+ interleavedReasoningField: sdk?.interleavedReasoningField
5353
5626
  }], clientModelId, debug);
5354
5627
  }
5355
5628
 
@@ -5366,7 +5639,11 @@ function localModelToRoute(lp, model) {
5366
5639
  modelFormat: model.modelFormat,
5367
5640
  contextWindow: model.contextWindow,
5368
5641
  npm: model.npm,
5369
- baseURL: model.apiBaseUrl
5642
+ baseURL: model.apiBaseUrl,
5643
+ providerId: lp.id,
5644
+ supportedParameters: model.supportedParameters,
5645
+ reasoning: model.reasoning,
5646
+ interleavedReasoningField: model.interleavedReasoningField
5370
5647
  };
5371
5648
  }
5372
5649
  function zenGoModelToRoute(model, apiKey) {
@@ -5384,7 +5661,8 @@ function zenGoModelToRoute(model, apiKey) {
5384
5661
  // openai-format Zen/Go models route through the SDK (openai-compatible);
5385
5662
  // anthropic models stay direct passthrough (no npm).
5386
5663
  npm: isAnthropic ? void 0 : "@ai-sdk/openai-compatible",
5387
- baseURL: isAnthropic ? void 0 : `${backend.baseUrl}/v1`
5664
+ baseURL: isAnthropic ? void 0 : `${backend.baseUrl}/v1`,
5665
+ providerId: model.sourceBackend
5388
5666
  };
5389
5667
  }
5390
5668
  function makeRouteResolver(localProviders, zenModels, goModels, zenGoApiKey) {
@@ -5554,6 +5832,7 @@ function cachedModelToLocal(cached, provider) {
5554
5832
  const apiUrl = cached.apiUrl ?? provider.api.url ?? "";
5555
5833
  const endpoint = resolveEndpoint(npm, apiUrl);
5556
5834
  if (endpoint === null) return null;
5835
+ const modelsDev = findModelsDevModel(provider.id, cached.id);
5557
5836
  const { id, upstreamModelId: upstreamModelId2 } = normalizeGoogleModelId(cached.id, npm);
5558
5837
  const normalizedUpstream = normalizeGoogleModelId(cached.upstreamModelId ?? cached.id, npm).upstreamModelId;
5559
5838
  const family = npm === "@ai-sdk/google" ? id.split(/[-/:]/)[0] ?? id : cached.family ?? "";
@@ -5569,7 +5848,10 @@ function cachedModelToLocal(cached, provider) {
5569
5848
  npm: npm || void 0,
5570
5849
  apiBaseUrl: apiUrl || void 0,
5571
5850
  cost: cached.cost,
5572
- contextWindow: cached.contextWindow ?? resolveContextWindow(id)
5851
+ contextWindow: cached.contextWindow ?? resolveContextWindow(id),
5852
+ supportedParameters: cached.supportedParameters,
5853
+ reasoning: cached.reasoning ?? modelsDev?.reasoning,
5854
+ interleavedReasoningField: cached.interleavedReasoningField ?? modelsDev?.interleaved?.field
5573
5855
  };
5574
5856
  }
5575
5857
  function materializeOne(provider, resolveCredential, agent) {
@@ -5705,8 +5987,8 @@ function countUsableZenGoModels(models) {
5705
5987
  }
5706
5988
  async function resolveProvidersForDisplay() {
5707
5989
  const reg = loadRegistry();
5708
- const registryIds = new Set(reg.providers.map((p19) => p19.id));
5709
5990
  const entries = [];
5991
+ const cloudProviders = reg.providers.filter((provider) => provider.id === "zen" || provider.id === "go");
5710
5992
  const opencodeKey = await readGlobalOpencodeCredential();
5711
5993
  let zenCount = 0;
5712
5994
  let goCount = 0;
@@ -5714,37 +5996,27 @@ async function resolveProvidersForDisplay() {
5714
5996
  const zenGo = await fetchZenGoModels(["zen", "go"]);
5715
5997
  zenCount = countUsableZenGoModels(zenGo.zenModels);
5716
5998
  goCount = countUsableZenGoModels(zenGo.goModels);
5717
- if (!registryIds.has("zen") && zenCount > 0) {
5718
- entries.push({
5719
- id: "zen",
5720
- name: "OpenCode Zen",
5721
- modelCount: zenCount,
5722
- enabled: true,
5723
- authLabel: "keychain (OpenCode API key)",
5724
- inRegistry: false,
5725
- cloudBuiltin: "zen"
5726
- });
5727
- }
5728
- if (!registryIds.has("go") && goCount > 0) {
5729
- entries.push({
5730
- id: "go",
5731
- name: "OpenCode Go",
5732
- modelCount: goCount,
5733
- enabled: true,
5734
- authLabel: "keychain (OpenCode API key)",
5735
- inRegistry: false,
5736
- cloudBuiltin: "go"
5737
- });
5738
- }
5999
+ }
6000
+ if (cloudProviders.length > 0 || zenCount + goCount > 0) {
6001
+ entries.push({
6002
+ id: "opencode-cloud",
6003
+ name: "OpenCode Zen / Go",
6004
+ modelCount: zenCount + goCount || cloudProviders.reduce(
6005
+ (total, provider) => total + (provider.modelsCache?.models.length ?? 0),
6006
+ 0
6007
+ ),
6008
+ enabled: cloudProviders.length === 0 || cloudProviders.some((provider) => provider.enabled),
6009
+ authLabel: cloudProviders[0] ? formatRegistryAuthLabel(cloudProviders[0]) : "keychain (OpenCode API key)",
6010
+ inRegistry: cloudProviders.length > 0,
6011
+ cloudBuiltin: "opencode"
6012
+ });
5739
6013
  }
5740
6014
  for (const provider of reg.providers) {
5741
- let modelCount = provider.modelsCache?.models.length ?? 0;
5742
- if (provider.id === "zen" && zenCount > 0) modelCount = zenCount;
5743
- if (provider.id === "go" && goCount > 0) modelCount = goCount;
6015
+ if (provider.id === "zen" || provider.id === "go") continue;
5744
6016
  entries.push({
5745
6017
  id: provider.id,
5746
6018
  name: provider.name,
5747
- modelCount,
6019
+ modelCount: provider.modelsCache?.models.length ?? 0,
5748
6020
  enabled: provider.enabled,
5749
6021
  authLabel: formatRegistryAuthLabel(provider),
5750
6022
  inRegistry: true
@@ -5770,7 +6042,10 @@ function localProvidersToServerModels(localProviders) {
5770
6042
  npm: model.modelFormat === "openai" ? model.npm || "@ai-sdk/openai-compatible" : model.npm,
5771
6043
  apiBaseUrl: model.apiBaseUrl,
5772
6044
  apiKey: provider.apiKey,
5773
- contextWindow: model.contextWindow
6045
+ contextWindow: model.contextWindow,
6046
+ supportedParameters: model.supportedParameters,
6047
+ reasoning: model.reasoning,
6048
+ interleavedReasoningField: model.interleavedReasoningField
5774
6049
  }))
5775
6050
  );
5776
6051
  }
@@ -5893,11 +6168,18 @@ async function askSaveServerPassword() {
5893
6168
 
5894
6169
  // src/server/router.ts
5895
6170
  import { createServer as createServer2 } from "http";
6171
+ function makeServerLog(debugLogPath) {
6172
+ if (!debugLogPath) return () => {
6173
+ };
6174
+ resetTraceLog(debugLogPath);
6175
+ return (msg) => writeSecureLogLine(debugLogPath, typeof msg === "function" ? msg() : msg);
6176
+ }
5896
6177
  async function startServer(options) {
5897
6178
  silenceSdkWarnings();
5898
6179
  const languageModelCache = /* @__PURE__ */ new Map();
6180
+ const plog = makeServerLog(options.debugLogPath);
5899
6181
  const server = createServer2((req, res) => {
5900
- void routeRequest(req, res, options, languageModelCache);
6182
+ void routeRequest(req, res, options, languageModelCache, plog);
5901
6183
  });
5902
6184
  await new Promise((resolve, reject2) => {
5903
6185
  server.once("error", reject2);
@@ -5920,9 +6202,10 @@ async function startServer(options) {
5920
6202
  })
5921
6203
  };
5922
6204
  }
5923
- async function routeRequest(req, res, options, modelCache) {
6205
+ async function routeRequest(req, res, options, modelCache, plog) {
5924
6206
  try {
5925
6207
  const pathname = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`).pathname;
6208
+ plog(`${req.method} ${pathname}`);
5926
6209
  if (req.method === "GET" && pathname === "/health") {
5927
6210
  sendJson(res, 200, { ok: true });
5928
6211
  return;
@@ -5944,7 +6227,7 @@ async function routeRequest(req, res, options, modelCache) {
5944
6227
  return;
5945
6228
  }
5946
6229
  if (req.method === "POST" && pathname === "/anthropic/v1/messages") {
5947
- await handleAnthropicMessages(req, res, options, modelCache);
6230
+ await handleAnthropicMessages(req, res, options, modelCache, plog);
5948
6231
  return;
5949
6232
  }
5950
6233
  if (req.method === "POST" && pathname === "/openai/v1/chat/completions") {
@@ -5956,14 +6239,18 @@ async function routeRequest(req, res, options, modelCache) {
5956
6239
  sendJson(res, 500, { error: { message: err instanceof Error ? err.message : String(err) } });
5957
6240
  }
5958
6241
  }
5959
- async function handleAnthropicMessages(req, res, options, modelCache) {
6242
+ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
5960
6243
  const body = await readJson(req);
5961
6244
  if (!body) {
5962
6245
  sendJson(res, 400, { error: { message: "Invalid JSON body" } });
5963
6246
  return;
5964
6247
  }
5965
6248
  const model = lookupModel(res, options.catalog, body.model);
5966
- if (!model) return;
6249
+ if (!model) {
6250
+ plog(`model not found: ${body.model}`);
6251
+ return;
6252
+ }
6253
+ plog(() => `anthropic-messages model=${body.model} format=${model.modelFormat} npm=${model.npm ?? "none"} stream=${body.stream}`);
5967
6254
  if (model.modelFormat === "anthropic") {
5968
6255
  if (model.baseUrl && !/^https?:\/\//i.test(model.baseUrl)) {
5969
6256
  sendJson(res, 400, { error: { message: `Invalid provider baseUrl: must be http:// or https://` } });
@@ -5971,7 +6258,10 @@ async function handleAnthropicMessages(req, res, options, modelCache) {
5971
6258
  }
5972
6259
  const messagesUrl = model.baseUrl ? `${model.baseUrl}/v1/messages` : `${backendFor(options, model).baseUrl}/v1/messages`;
5973
6260
  const apiKey = model.apiKey ?? options.apiKey;
5974
- await forwardJson(res, messagesUrl, { ...body, model: upstreamModelId(model) }, apiKey);
6261
+ const betaHeaderRaw = req.headers["anthropic-beta"];
6262
+ const inboundBeta = Array.isArray(betaHeaderRaw) ? betaHeaderRaw.join(",") : betaHeaderRaw;
6263
+ plog(() => `anthropic-passthrough \u2192 ${messagesUrl}`);
6264
+ await forwardJson(res, messagesUrl, { ...body, model: upstreamModelId(model) }, apiKey, inboundBeta);
5975
6265
  return;
5976
6266
  }
5977
6267
  if (model.modelFormat === "openai") {
@@ -5980,22 +6270,32 @@ async function handleAnthropicMessages(req, res, options, modelCache) {
5980
6270
  return;
5981
6271
  }
5982
6272
  const apiKey = model.apiKey ?? options.apiKey;
5983
- let languageModel = modelCache.get(model.id);
6273
+ const cacheKey = sdkModelCacheKey(model);
6274
+ let languageModel = modelCache.get(cacheKey);
5984
6275
  if (!languageModel) {
5985
6276
  languageModel = await createLanguageModel({
5986
6277
  npm: model.npm,
5987
6278
  modelId: upstreamModelId(model),
5988
6279
  apiKey,
5989
6280
  baseURL: model.apiBaseUrl,
5990
- providerId: model.sourceBackend,
6281
+ providerId: model.providerId ?? model.sourceBackend,
5991
6282
  vertex: options.vertex
5992
6283
  });
5993
- modelCache.set(model.id, languageModel);
6284
+ modelCache.set(cacheKey, languageModel);
5994
6285
  }
5995
6286
  const params = translateRequest(body, model.npm, {
5996
- defaultEffort: anthropicEffortFromRequest(body) ? void 0 : model.defaultEffort
6287
+ defaultEffort: anthropicEffortFromRequest(body) ? void 0 : model.defaultEffort,
6288
+ reasoningMetadata: {
6289
+ providerId: model.providerId,
6290
+ apiBaseUrl: model.apiBaseUrl,
6291
+ supportedParameters: model.supportedParameters,
6292
+ reasoning: model.reasoning,
6293
+ interleavedReasoningField: model.interleavedReasoningField
6294
+ }
5997
6295
  });
5998
6296
  const clientWantsStream = Boolean(body.stream);
6297
+ const responseModelId = options.gateway?.maskGatewayIds ? gatewayDisplayName(model, options.gateway) : typeof body.model === "string" ? body.model : model.id;
6298
+ plog(() => `sdk npm=${model.npm} upstream=${upstreamModelId(model)} responseModel=${responseModelId} stream=${clientWantsStream}`);
5999
6299
  try {
6000
6300
  if (clientWantsStream) {
6001
6301
  res.writeHead(200, {
@@ -6003,12 +6303,10 @@ async function handleAnthropicMessages(req, res, options, modelCache) {
6003
6303
  "Cache-Control": "no-cache",
6004
6304
  "Connection": "keep-alive"
6005
6305
  });
6006
- const clientModel = typeof body.model === "string" ? body.model : model.id;
6007
- await streamAnthropicResponse(languageModel, params, clientModel, (chunk) => res.write(chunk));
6306
+ await streamAnthropicResponse(languageModel, params, responseModelId, (chunk) => res.write(chunk));
6008
6307
  res.end();
6009
6308
  } else {
6010
- const clientModel = typeof body.model === "string" ? body.model : model.id;
6011
- const anthropicResponse = await generateAnthropicResponse(languageModel, params, clientModel);
6309
+ const anthropicResponse = await generateAnthropicResponse(languageModel, params, responseModelId);
6012
6310
  sendJson(res, 200, anthropicResponse);
6013
6311
  }
6014
6312
  } catch (err) {
@@ -6029,6 +6327,14 @@ async function handleOpenAIChatCompletions(req, res, options) {
6029
6327
  const model = lookupModel(res, options.catalog, body.model);
6030
6328
  if (!model) return;
6031
6329
  if (model.modelFormat === "openai") {
6330
+ if (!isOpenAIChatCompletionsModel(model)) {
6331
+ sendJson(res, 400, {
6332
+ error: {
6333
+ message: `OpenAI chat completions are not available for model: ${model.id}. Use /anthropic/v1/messages.`
6334
+ }
6335
+ });
6336
+ return;
6337
+ }
6032
6338
  if (model.completionsUrl && !/^https?:\/\//i.test(model.completionsUrl)) {
6033
6339
  sendJson(res, 400, { error: { message: `Invalid provider completionsUrl: must be http:// or https://` } });
6034
6340
  return;
@@ -6064,15 +6370,22 @@ function backendFor(options, model) {
6064
6370
  if (model.sourceBackend === "go") return options.backends.go;
6065
6371
  throw new Error(`Provider ${model.sourceBackend} is not a cloud backend \u2014 model must set baseUrl/completionsUrl`);
6066
6372
  }
6067
- async function forwardJson(res, url, body, apiKey) {
6068
- const upstream = await postJsonUpstream(url, body, apiKey);
6373
+ function sdkModelCacheKey(model) {
6374
+ return [
6375
+ model.providerId ?? model.sourceBackend,
6376
+ model.id,
6377
+ upstreamModelId(model),
6378
+ model.npm ?? "",
6379
+ model.apiBaseUrl ?? ""
6380
+ ].join("");
6381
+ }
6382
+ async function forwardJson(res, url, body, apiKey, inboundBeta) {
6383
+ const upstream = await postJsonUpstream(url, body, apiKey, inboundBeta);
6069
6384
  sendJson(res, upstream.status, upstream.body);
6070
6385
  }
6071
6386
  async function readJson(req) {
6072
6387
  try {
6073
- const chunks = [];
6074
- for await (const chunk of req) chunks.push(Buffer.from(chunk));
6075
- const raw = Buffer.concat(chunks).toString();
6388
+ const raw = await readBody(req);
6076
6389
  return raw ? JSON.parse(raw) : {};
6077
6390
  } catch {
6078
6391
  return null;
@@ -6228,7 +6541,9 @@ function resolveVertexLocation(env = process.env) {
6228
6541
  function defaultAdcCredentialsPath(home = homedir7()) {
6229
6542
  return join10(home, ".config", "gcloud", "application_default_credentials.json");
6230
6543
  }
6231
- function hasApplicationDefaultCredentials(home = homedir7(), adcPath = defaultAdcCredentialsPath(home)) {
6544
+ function hasApplicationDefaultCredentials(home = homedir7(), adcPath = defaultAdcCredentialsPath(home), env = process.env) {
6545
+ const explicitPath = env["GOOGLE_APPLICATION_CREDENTIALS"]?.trim();
6546
+ if (explicitPath && existsSync10(explicitPath)) return true;
6232
6547
  return existsSync10(adcPath);
6233
6548
  }
6234
6549
  function loadVertexModelEntries(env = process.env) {
@@ -6259,19 +6574,23 @@ function buildVertexRuntimeConfig(env = process.env) {
6259
6574
  };
6260
6575
  }
6261
6576
  function vertexModelsToServerModels(config) {
6262
- return config.models.map((model) => ({
6263
- id: model.id,
6264
- name: model.display_name,
6265
- isFree: false,
6266
- brand: "Anthropic",
6267
- sourceBackend: "vertex",
6268
- modelFormat: "openai",
6269
- upstreamModelId: model.upstream_id ?? model.id,
6270
- npm: VERTEX_ANTHROPIC_NPM,
6271
- providerLabel: "Vertex AI",
6272
- providerId: "vertex",
6273
- contextWindow: resolveContextWindow(model.id)
6274
- }));
6577
+ return config.models.map((model) => {
6578
+ const caps = getReasoningCapabilities(VERTEX_ANTHROPIC_NPM, model.upstream_id ?? model.id);
6579
+ return {
6580
+ id: model.id,
6581
+ name: model.display_name,
6582
+ isFree: false,
6583
+ brand: "Anthropic",
6584
+ sourceBackend: "vertex",
6585
+ modelFormat: "openai",
6586
+ upstreamModelId: model.upstream_id ?? model.id,
6587
+ npm: VERTEX_ANTHROPIC_NPM,
6588
+ providerLabel: "Vertex AI",
6589
+ providerId: "vertex",
6590
+ contextWindow: resolveContextWindow(model.id),
6591
+ ...caps.defaultLevel ? { defaultEffort: caps.defaultLevel } : {}
6592
+ };
6593
+ });
6275
6594
  }
6276
6595
  function vertexClientModelLookupCandidates(modelId) {
6277
6596
  const candidates = [modelId];
@@ -6388,7 +6707,13 @@ async function loadServerModels() {
6388
6707
  }
6389
6708
  function enrichServerModelReasoning(model) {
6390
6709
  if (!model.npm || model.modelFormat !== "openai") return model;
6391
- const caps = getReasoningCapabilities(model.npm, upstreamModelId(model));
6710
+ const caps = getReasoningCapabilities(model.npm, upstreamModelId(model), {
6711
+ providerId: model.providerId,
6712
+ apiBaseUrl: model.apiBaseUrl,
6713
+ supportedParameters: model.supportedParameters,
6714
+ reasoning: model.reasoning,
6715
+ interleavedReasoningField: model.interleavedReasoningField
6716
+ });
6392
6717
  if (!caps.defaultLevel) return model;
6393
6718
  return { ...model, defaultEffort: caps.defaultLevel };
6394
6719
  }
@@ -6974,6 +7299,71 @@ async function pickGlobalFavoriteModel(providers, favorites) {
6974
7299
  }
6975
7300
  }
6976
7301
 
7302
+ // src/favorites-resolver.ts
7303
+ var ZEN_GO_PROVIDER_NAME = {
7304
+ zen: "OpenCode Zen",
7305
+ go: "OpenCode Go"
7306
+ };
7307
+ function resolveFavorite(fav, ctx) {
7308
+ if (fav.providerId === "zen" || fav.providerId === "go") {
7309
+ if (!ctx.zenGoApiKey) return void 0;
7310
+ const models = fav.providerId === "zen" ? ctx.zenModels : ctx.goModels;
7311
+ const model = models?.find((m) => m.id === fav.modelId);
7312
+ if (!model) return void 0;
7313
+ return {
7314
+ providerId: fav.providerId,
7315
+ providerName: ZEN_GO_PROVIDER_NAME[fav.providerId],
7316
+ model,
7317
+ apiKey: ctx.zenGoApiKey,
7318
+ sourceBackend: fav.providerId
7319
+ };
7320
+ }
7321
+ if (ctx.findLocalModel) {
7322
+ const found = ctx.findLocalModel(fav.providerId, fav.modelId);
7323
+ if (!found) return void 0;
7324
+ if (ctx.agent && shouldHideModel({ providerId: fav.providerId, modelId: fav.modelId, agent: ctx.agent })) {
7325
+ return void 0;
7326
+ }
7327
+ return {
7328
+ providerId: fav.providerId,
7329
+ providerName: found.provider.name,
7330
+ model: found.model,
7331
+ apiKey: found.provider.apiKey
7332
+ };
7333
+ }
7334
+ return void 0;
7335
+ }
7336
+ function buildFavoritesList(starting, favorites, ctx, max = 20) {
7337
+ const droppedFavorites = [];
7338
+ const seen = /* @__PURE__ */ new Set();
7339
+ const out = [];
7340
+ if (starting) {
7341
+ seen.add(`${starting.providerId}::${starting.model.id}`);
7342
+ out.push(starting);
7343
+ }
7344
+ for (const fav of favorites) {
7345
+ if (out.length >= max) break;
7346
+ const key = `${fav.providerId}::${fav.modelId}`;
7347
+ if (seen.has(key)) continue;
7348
+ const resolved = resolveFavorite(fav, ctx);
7349
+ if (!resolved) {
7350
+ droppedFavorites.push(fav);
7351
+ continue;
7352
+ }
7353
+ seen.add(key);
7354
+ out.push(resolved);
7355
+ }
7356
+ return { resolved: out, droppedFavorites };
7357
+ }
7358
+ function resolveFirstAvailableFavorite(favorites, providers) {
7359
+ for (const fav of favorites) {
7360
+ const provider = providers.find((lp) => lp.id === fav.providerId);
7361
+ const model = provider?.models.find((m) => m.id === fav.modelId);
7362
+ if (provider && model) return { provider, model };
7363
+ }
7364
+ return void 0;
7365
+ }
7366
+
6977
7367
  // src/providers-command.ts
6978
7368
  import pc10 from "picocolors";
6979
7369
  import * as p10 from "@clack/prompts";
@@ -7136,18 +7526,35 @@ function modelInfoToCached(m, npm, apiUrl) {
7136
7526
  async function refreshZenGoProvider(provider) {
7137
7527
  const backendId = provider.id === "go" || provider.templateId === "go" ? "go" : "zen";
7138
7528
  const result = await getModels(BACKENDS[backendId]);
7139
- return result.models.filter((m) => m.modelFormat !== "unsupported").map((m) => modelInfoToCached(m, "@ai-sdk/openai-compatible", `${BACKENDS[backendId].baseUrl}/v1`));
7529
+ return result.models.filter((m) => m.modelFormat !== "unsupported").map((m) => {
7530
+ const isAnthropic = m.modelFormat === "anthropic";
7531
+ const npm = isAnthropic ? "@ai-sdk/anthropic" : "@ai-sdk/openai-compatible";
7532
+ const apiUrl = isAnthropic ? BACKENDS[backendId].baseUrl : `${BACKENDS[backendId].baseUrl}/v1`;
7533
+ return modelInfoToCached(m, npm, apiUrl);
7534
+ });
7140
7535
  }
7141
7536
  async function refreshApiListProvider(provider, apiKey) {
7142
7537
  const npm = provider.api.npm ?? "@ai-sdk/openai-compatible";
7143
7538
  const catalogTemplate = resolveProviderTemplate(provider);
7144
7539
  const baseUrl = effectiveProviderBaseUrl(provider, catalogTemplate);
7145
- const template = catalogTemplate ?? syntheticTemplate(provider, baseUrl);
7146
7540
  if (!baseUrl) {
7147
7541
  return { models: [], error: "Provider has no API base URL configured." };
7148
7542
  }
7543
+ let safeBaseUrl = baseUrl;
7544
+ const configuredUrl = provider.api.url?.trim();
7545
+ const templateDefault = catalogTemplate?.defaultBaseUrl?.trim();
7546
+ if (configuredUrl && configuredUrl !== templateDefault) {
7547
+ const urlCheck = await validateCustomEndpointUrl(baseUrl, {
7548
+ allowInsecureLocal: catalogTemplate?.apiKeyOptional === true
7549
+ });
7550
+ if (!urlCheck.ok || !urlCheck.normalizedUrl) {
7551
+ return { models: [], error: `${urlCheck.error ?? "Invalid API base URL."} ${urlCheck.hint ?? ""}`.trim() };
7552
+ }
7553
+ safeBaseUrl = urlCheck.normalizedUrl;
7554
+ }
7555
+ const template = catalogTemplate ?? syntheticTemplate(provider, safeBaseUrl);
7149
7556
  if (npm === "@ai-sdk/anthropic") {
7150
- const fetched2 = await fetchAnthropicModels(baseUrl, apiKey);
7557
+ const fetched2 = await fetchAnthropicModels(safeBaseUrl, apiKey);
7151
7558
  if (fetched2.error || fetched2.models.length === 0) {
7152
7559
  return { models: [], error: fetched2.error ?? "No models returned.", baseUrl: fetched2.baseUrl };
7153
7560
  }
@@ -7156,7 +7563,7 @@ async function refreshApiListProvider(provider, apiKey) {
7156
7563
  baseUrl: fetched2.baseUrl
7157
7564
  };
7158
7565
  }
7159
- const fetched = await fetchTemplateModels(template, apiKey, baseUrl);
7566
+ const fetched = await fetchTemplateModels(template, apiKey, safeBaseUrl);
7160
7567
  if (fetched.error || fetched.models.length === 0) {
7161
7568
  return { models: [], error: fetched.error ?? "No models returned." };
7162
7569
  }
@@ -7206,7 +7613,10 @@ async function refreshProviderModels(providerId, apiKey, registry = loadRegistry
7206
7613
  if (source === "zen-go-api") {
7207
7614
  models = await refreshZenGoProvider(provider);
7208
7615
  } else {
7209
- if (isLikelyPlaceholderKey(apiKey)) {
7616
+ const template = resolveProviderTemplate(provider);
7617
+ const keyOptional = template?.apiKeyOptional === true;
7618
+ const effectiveKey = keyOptional && isLikelyPlaceholderKey(apiKey) ? "" : apiKey;
7619
+ if (!keyOptional && isLikelyPlaceholderKey(effectiveKey)) {
7210
7620
  if (cachedModelCount(provider) > 0) {
7211
7621
  return skipWithCachedModels(
7212
7622
  provider,
@@ -7220,7 +7630,7 @@ async function refreshProviderModels(providerId, apiKey, registry = loadRegistry
7220
7630
  reason: "No usable API key \u2014 add the provider via relay-ai providers add with a real key."
7221
7631
  };
7222
7632
  }
7223
- if (!apiKey) {
7633
+ if (!keyOptional && !effectiveKey) {
7224
7634
  return {
7225
7635
  id: provider.id,
7226
7636
  name: provider.name,
@@ -7228,7 +7638,7 @@ async function refreshProviderModels(providerId, apiKey, registry = loadRegistry
7228
7638
  reason: "API key not available \u2014 cannot refresh models."
7229
7639
  };
7230
7640
  }
7231
- const fetched = await refreshApiListProvider(provider, apiKey);
7641
+ const fetched = await refreshApiListProvider(provider, effectiveKey ?? "");
7232
7642
  if (fetched.error) {
7233
7643
  if ((fetched.error.includes("rejected") || fetched.error.includes("401") || fetched.error.includes("403")) && cachedModelCount(provider) > 0) {
7234
7644
  return skipWithCachedModels(
@@ -7252,7 +7662,7 @@ async function refreshProviderModels(providerId, apiKey, registry = loadRegistry
7252
7662
  name: provider.name,
7253
7663
  ok: true,
7254
7664
  modelCount: enriched.length,
7255
- previousModelCount
7665
+ previousModelCount: provider.refreshedAt ? previousModelCount : void 0
7256
7666
  };
7257
7667
  } catch (err) {
7258
7668
  return {
@@ -7425,7 +7835,10 @@ async function authenticateProvider(providerId, options = {}) {
7425
7835
  if (!supportsNativeOAuth(providerId)) {
7426
7836
  if (findOpencodeBinary()) {
7427
7837
  const cred2 = await runOpencodeAuthBroker(providerId, { method: options.brokerMethod });
7428
- await saveProviderCredential(oauthAuthRef(providerId), oauthCredentialToKeychainJson(cred2));
7838
+ const saved2 = await saveProviderCredential(oauthAuthRef(providerId), oauthCredentialToKeychainJson(cred2));
7839
+ if (!saved2) {
7840
+ p9.log.warn("Could not save OAuth tokens to Keychain \u2014 session may not persist.");
7841
+ }
7429
7842
  const registryProvider2 = await upsertOAuthProvider(providerId, cred2);
7430
7843
  return { providerId, credential: cred2, registryProvider: registryProvider2 };
7431
7844
  }
@@ -7547,7 +7960,7 @@ ${pc10.bold("Usage:")}
7547
7960
  ${pc10.bold("Subcommands:")}
7548
7961
  (none) Provider hub wizard ${pc10.dim("[Phase 1.1]")}
7549
7962
  add Add a provider (Groq, Mistral, Together AI, \u2026) ${pc10.dim("[Phase 1.1]")}
7550
- import Bring settings from OpenCode (one-time) ${pc10.dim("[Phase 1.0]")}
7963
+ import import providers from 'open code CLI' (one-time) ${pc10.dim("[Phase 1.0]")}
7551
7964
  auth Sign in with OAuth (xAI, OpenAI ChatGPT) ${pc10.dim("[Phase 2]")}
7552
7965
  list Show configured providers ${pc10.dim("[Phase 1.0]")}
7553
7966
  remove Remove a provider by id ${pc10.dim("[Phase 1.1]")}
@@ -7596,7 +8009,7 @@ async function runProvidersImport() {
7596
8009
  );
7597
8010
  if (result.skipped.length > 0) {
7598
8011
  for (const s of result.skipped) {
7599
- const reason = s.reason === "user-skipped" ? "skipped by you" : s.reason === "conflict-kept" ? "kept your existing config" : s.reason === "oauth-no-token" ? "OAuth provider in OpenCode but not signed in \u2014 run relay-ai providers auth" : s.reason === "no-api-key" ? "no API key in OpenCode \u2014 add key there or use relay-ai providers add" : s.reason === "manual-only" ? "uses gcloud/AWS credentials \u2014 not importable via API key" : s.reason === "placeholder-key" ? "placeholder API key \u2014 provider not imported" : s.reason === "invalid-key" ? "API key failed verification \u2014 provider not imported" : s.reason;
8012
+ const reason = s.reason === "user-skipped" ? "skipped by you" : s.reason === "conflict-kept" ? "kept your existing config" : s.reason === "oauth-no-token" ? "OAuth provider in OpenCode but not signed in \u2014 run relay-ai providers auth" : s.reason === "no-api-key" ? "no API key in OpenCode \u2014 add key there or use relay-ai providers add" : s.reason === "manual-only" ? "uses gcloud/AWS credentials \u2014 not importable via API key" : s.reason === "placeholder-key" ? "placeholder API key \u2014 provider not imported" : s.reason === "invalid-key" ? "API key failed verification \u2014 provider not imported" : s.reason === "credential-save-failed" ? "could not save credential \u2014 provider not imported" : s.reason;
7600
8013
  p10.log.warn(`Skipped ${s.name} (${s.id}): ${reason}`);
7601
8014
  }
7602
8015
  }
@@ -7607,6 +8020,19 @@ async function runProvidersImport() {
7607
8020
  }
7608
8021
  }
7609
8022
  }
8023
+ if (result.imported.length > 0) {
8024
+ const refreshSpinner = p10.spinner();
8025
+ refreshSpinner.start("Fetching model capabilities from providers...");
8026
+ const registry2 = loadRegistry();
8027
+ for (const provider of result.imported) {
8028
+ const key = await resolveRefreshCredential(
8029
+ provider,
8030
+ async (pr) => resolveProviderCredential(pr.id, pr.authRef)
8031
+ );
8032
+ await refreshProviderModels(provider.id, key, registry2);
8033
+ }
8034
+ refreshSpinner.stop("Model capabilities refreshed.");
8035
+ }
7610
8036
  return 0;
7611
8037
  }
7612
8038
  async function runProvidersAuth(providerId, method) {
@@ -7649,8 +8075,8 @@ async function runProvidersRefreshModels(providerId) {
7649
8075
  p10.log.error(`${result.name}: ${result.reason ?? "Refresh failed."}`);
7650
8076
  return 1;
7651
8077
  }
7652
- const diff = (result.modelCount ?? 0) - (result.previousModelCount ?? 0);
7653
- const diffStr = diff > 0 ? ` (+${diff})` : diff < 0 ? ` (${diff})` : "";
8078
+ const diff = result.previousModelCount === void 0 ? 0 : (result.modelCount ?? 0) - result.previousModelCount;
8079
+ const diffStr = result.previousModelCount === void 0 ? "" : diff > 0 ? ` (+${diff})` : diff < 0 ? ` (${diff})` : "";
7654
8080
  p10.log.success(`${result.name}: ${result.modelCount} model${result.modelCount === 1 ? "" : "s"} updated${diffStr}.`);
7655
8081
  return 0;
7656
8082
  }
@@ -7664,8 +8090,8 @@ async function runProvidersRefreshModels(providerId) {
7664
8090
  if (ok.length > 0) {
7665
8091
  p10.log.success(`Updated ${ok.length} provider${ok.length === 1 ? "" : "s"}.`);
7666
8092
  for (const r of ok) {
7667
- const diff = (r.modelCount ?? 0) - (r.previousModelCount ?? 0);
7668
- const diffStr = diff > 0 ? ` (+${diff})` : diff < 0 ? ` (${diff})` : "";
8093
+ const diff = r.previousModelCount === void 0 ? 0 : (r.modelCount ?? 0) - r.previousModelCount;
8094
+ const diffStr = r.previousModelCount === void 0 ? "" : diff > 0 ? ` (+${diff})` : diff < 0 ? ` (${diff})` : "";
7669
8095
  p10.log.info(` ${r.name}: ${r.modelCount} model${r.modelCount === 1 ? "" : "s"}${diffStr}`);
7670
8096
  }
7671
8097
  }
@@ -7696,7 +8122,9 @@ async function runProvidersList() {
7696
8122
  }
7697
8123
  async function pickTemplateFromCatalog() {
7698
8124
  while (true) {
7699
- const templates = listAddableTemplates(loadRegistry().providers.map((p19) => p19.id));
8125
+ const registry = loadRegistry();
8126
+ const configuredIds = new Set(registry.providers.map((p19) => p19.id));
8127
+ const templates = listAddableTemplates(configuredIds);
7700
8128
  if (templates.length === 0) return null;
7701
8129
  const method = await p10.select({
7702
8130
  message: `Choose a provider (${templates.length} available)`,
@@ -7727,22 +8155,12 @@ async function pickTemplateFromCatalog() {
7727
8155
  const query = String(searchInput);
7728
8156
  const matched = filterTemplates(templates, query);
7729
8157
  if (matched.length === 0) {
7730
- if (matchesOpenCodeCloudSearch(query)) {
7731
- p10.log.info(
7732
- "OpenCode Zen / Go are cloud backends \u2014 not in the Groq/Mistral template list."
7733
- );
7734
- const addCloud = await p10.confirm({
7735
- message: "Add OpenCode Zen / Go with your OpenCode API key?",
7736
- initialValue: true
7737
- });
7738
- if (p10.isCancel(addCloud)) continue;
7739
- if (addCloud) {
7740
- await runOpenCodeCloudAddFlow();
7741
- return null;
7742
- }
7743
- continue;
8158
+ const alreadyAdded = filterTemplates(listSupportedTemplates(), query).filter((t) => configuredIds.has(t.id));
8159
+ if (alreadyAdded.length > 0) {
8160
+ p10.log.info(`Already configured: ${alreadyAdded.map((t) => t.name).join(", ")}`);
8161
+ } else {
8162
+ p10.log.warn("No providers match \u2014 try a different search");
7744
8163
  }
7745
- p10.log.warn("No providers match \u2014 try a different search");
7746
8164
  continue;
7747
8165
  }
7748
8166
  const options = matched.map((t) => ({
@@ -7766,6 +8184,46 @@ async function runTemplateAddFlow() {
7766
8184
  }
7767
8185
  const template = await pickTemplateFromCatalog();
7768
8186
  if (!template) return 0;
8187
+ if (template.modelSource === "zen-go-api") {
8188
+ const existingKey = await readGlobalOpencodeCredential();
8189
+ let apiKey2 = existingKey;
8190
+ if (!apiKey2) {
8191
+ printPanel(pc10.cyan("OpenCode cloud"), [
8192
+ `${pc10.white("Get an API key at:")} ${fmtUrl("https://opencode.ai/auth")}`,
8193
+ `${pc10.dim("Uses OpenCode Zen / Go cloud models \u2014 not the same as importing from the OpenCode CLI.")}`
8194
+ ]);
8195
+ const collected = await resolveOrCollectApiKey(false, false);
8196
+ if (!collected) {
8197
+ p10.cancel("Cancelled.");
8198
+ return 0;
8199
+ }
8200
+ apiKey2 = collected;
8201
+ }
8202
+ await migrateGlobalOpencodeCredential();
8203
+ const spinner10 = p10.spinner();
8204
+ spinner10.start(`Adding ${template.name}...`);
8205
+ const zenStub = addZenRegistryStub();
8206
+ const goStub = addGoRegistryStub();
8207
+ if (!zenStub.added && !goStub.added) {
8208
+ spinner10.stop("");
8209
+ p10.log.warn("OpenCode Zen / Go is already configured.");
8210
+ return 0;
8211
+ }
8212
+ const registry = loadRegistry();
8213
+ const refreshResults = [
8214
+ await refreshProviderModels("zen", apiKey2, registry),
8215
+ await refreshProviderModels("go", apiKey2, registry)
8216
+ ];
8217
+ spinner10.stop("");
8218
+ const modelCount = refreshResults.reduce((total, result2) => total + (result2.modelCount ?? 0), 0);
8219
+ const failed = refreshResults.filter((result2) => !result2.ok);
8220
+ if (failed.length === 0) {
8221
+ p10.log.success(`Added ${template.name} \u2014 ${fmtCount(modelCount, "model")} updated.`);
8222
+ } else {
8223
+ p10.log.warn(`Added ${template.name}, but ${failed.length} catalog refresh${failed.length === 1 ? "" : "es"} failed.`);
8224
+ }
8225
+ return 0;
8226
+ }
7769
8227
  if (template.signupUrl) {
7770
8228
  printPanel(fmtProvider(template.name), [
7771
8229
  `${pc10.white("Get an API key at:")} ${fmtUrl(template.signupUrl)}`
@@ -7811,66 +8269,6 @@ async function runTemplateAddFlow() {
7811
8269
  logConnected(template.name, result.modelCount ?? 0);
7812
8270
  return 0;
7813
8271
  }
7814
- async function runOpenCodeCloudAddFlow() {
7815
- const registry = loadRegistry();
7816
- const hasZen = registry.providers.some((pr) => pr.id === "zen");
7817
- const hasGo = registry.providers.some((pr) => pr.id === "go");
7818
- if (hasZen && hasGo) {
7819
- p10.log.info("OpenCode Zen and Go are already configured.");
7820
- return 0;
7821
- }
7822
- printPanel(pc10.cyan("OpenCode cloud"), [
7823
- `${pc10.white("Get an API key at:")} ${fmtUrl("https://opencode.ai/auth")}`,
7824
- `${pc10.dim("Uses OpenCode Zen / Go cloud models \u2014 not the same as importing from the OpenCode CLI.")}`
7825
- ]);
7826
- const existingKey = await readGlobalOpencodeCredential();
7827
- if (!existingKey) {
7828
- const apiKey = await resolveOrCollectApiKey(false, false);
7829
- if (!apiKey) {
7830
- p10.cancel("Cancelled.");
7831
- return 0;
7832
- }
7833
- }
7834
- await migrateGlobalOpencodeCredential();
7835
- let pick;
7836
- if (!hasZen && !hasGo) {
7837
- const choice = await p10.select({
7838
- message: "Which OpenCode cloud backend?",
7839
- options: [
7840
- { value: "zen", label: "OpenCode Zen", hint: "Free + paid models" },
7841
- { value: "go", label: "OpenCode Go", hint: "Paid models" },
7842
- { value: "both", label: "Both Zen and Go", hint: "Same API key" }
7843
- ]
7844
- });
7845
- if (p10.isCancel(choice)) {
7846
- p10.cancel("Cancelled.");
7847
- return 0;
7848
- }
7849
- pick = choice;
7850
- } else if (!hasZen) {
7851
- pick = "zen";
7852
- } else {
7853
- pick = "go";
7854
- }
7855
- const added = [];
7856
- if ((pick === "zen" || pick === "both") && !hasZen) {
7857
- const zen = addZenRegistryStub();
7858
- if (zen.added) added.push("OpenCode Zen");
7859
- else if (zen.reason) p10.log.warn(zen.reason);
7860
- }
7861
- if ((pick === "go" || pick === "both") && !hasGo) {
7862
- const go = addGoRegistryStub();
7863
- if (go.added) added.push("OpenCode Go");
7864
- else if (go.reason) p10.log.warn(go.reason);
7865
- }
7866
- if (added.length === 0) {
7867
- p10.log.info("Nothing new to add.");
7868
- return 0;
7869
- }
7870
- p10.log.success(`Added ${added.join(" and ")}.`);
7871
- p10.log.info("Run relay-ai providers refresh-models to cache model lists.");
7872
- return 0;
7873
- }
7874
8272
  async function runCustomEndpointAddFlow() {
7875
8273
  const kindChoice = await p10.select({
7876
8274
  message: "Custom server type",
@@ -7932,14 +8330,9 @@ async function runProvidersAdd() {
7932
8330
  const registry = loadRegistry();
7933
8331
  const hasOpencode = findOpencodeBinary() !== null;
7934
8332
  const options = [
7935
- {
7936
- value: "opencode-cloud",
7937
- label: "OpenCode Zen / Go (cloud API key)",
7938
- hint: "Key from opencode.ai/auth \u2014 not in the Groq/Mistral list"
7939
- },
7940
8333
  {
7941
8334
  value: "import",
7942
- label: "Bring settings from OpenCode CLI",
8335
+ label: "import providers from 'open code CLI'",
7943
8336
  hint: hasOpencode ? "Import Groq, OpenAI, etc. from your OpenCode config" : "Requires OpenCode CLI"
7944
8337
  }
7945
8338
  ];
@@ -7961,9 +8354,6 @@ async function runProvidersAdd() {
7961
8354
  p10.cancel("Cancelled.");
7962
8355
  return 0;
7963
8356
  }
7964
- if (choice === "opencode-cloud") {
7965
- return runOpenCodeCloudAddFlow();
7966
- }
7967
8357
  if (choice === "import") {
7968
8358
  if (!hasOpencode) {
7969
8359
  p10.log.error("OpenCode CLI not found. Install from https://opencode.ai");
@@ -8003,11 +8393,30 @@ async function runProvidersRemove(id, interactive = false) {
8003
8393
  }
8004
8394
  return 0;
8005
8395
  }
8006
- async function runCloudBuiltinDetail(id) {
8007
- const name = id === "zen" ? "OpenCode Zen" : "OpenCode Go";
8008
- printCloudProviderPanel(name);
8396
+ async function runOpenCodeCloudDetail() {
8397
+ const registry = loadRegistry();
8398
+ const routes = registry.providers.filter((provider) => provider.id === "zen" || provider.id === "go");
8399
+ printCloudProviderPanel("OpenCode Zen / Go");
8400
+ if (routes.length === 0) return "back";
8401
+ const choice = await p10.select({
8402
+ message: "Manage an OpenCode catalog",
8403
+ options: [
8404
+ ...routes.map((provider) => ({
8405
+ value: provider.id,
8406
+ label: provider.name,
8407
+ hint: `${provider.modelsCache?.models.length ?? 0} cached models`
8408
+ })),
8409
+ { value: "back", label: "Back", hint: "" }
8410
+ ]
8411
+ });
8412
+ if (!p10.isCancel(choice) && choice !== "back") {
8413
+ await runProviderDetail(String(choice));
8414
+ }
8009
8415
  return "back";
8010
8416
  }
8417
+ function providerHubChoiceValue(entry) {
8418
+ return entry.cloudBuiltin ? `cloud:${entry.cloudBuiltin}` : `provider:${entry.id}`;
8419
+ }
8011
8420
  async function runProviderDetail(id) {
8012
8421
  const registry = loadRegistry();
8013
8422
  const provider = registry.providers.find((pr) => pr.id === id);
@@ -8065,23 +8474,24 @@ async function runProvidersHub() {
8065
8474
  const hasOpencode = findOpencodeBinary() !== null;
8066
8475
  while (true) {
8067
8476
  const entries = await resolveProvidersForDisplay();
8068
- const options = [];
8477
+ const options = [
8478
+ { value: "add", label: pc10.bold("+ Add a provider"), hint: "" }
8479
+ ];
8069
8480
  for (const entry of entries) {
8070
8481
  const hint = entry.id;
8071
- const value = `provider:${entry.id}`;
8482
+ const value = providerHubChoiceValue(entry);
8072
8483
  options.push({
8073
8484
  value,
8074
8485
  label: providerLabel(entry.name, entry.modelCount, entry.enabled),
8075
8486
  hint
8076
8487
  });
8077
8488
  }
8078
- options.push({ value: "add", label: "+ Add a provider", hint: "" });
8079
8489
  options.push({ value: "auth-menu", label: "\u2192 Sign in with OAuth (xAI / OpenAI)", hint: "Device code or OpenCode broker" });
8080
8490
  if (entries.length > 0) {
8081
8491
  options.push({ value: "refresh-all", label: "\u21BA Refresh all models", hint: "Update model lists for all providers" });
8082
8492
  }
8083
8493
  if (hasOpencode) {
8084
- options.push({ value: "import", label: "\u2192 Bring settings from OpenCode CLI", hint: "One-time import" });
8494
+ options.push({ value: "import", label: "\u2192 import providers from 'open code CLI'", hint: "One-time import" });
8085
8495
  }
8086
8496
  options.push({ value: "done", label: "Done", hint: "" });
8087
8497
  const choice = await p10.select({
@@ -8116,7 +8526,7 @@ async function runProvidersHub() {
8116
8526
  }
8117
8527
  if (typeof choice === "string" && choice.startsWith("cloud:")) {
8118
8528
  const id = choice.slice("cloud:".length);
8119
- if (id === "zen" || id === "go") await runCloudBuiltinDetail(id);
8529
+ if (id === "opencode") await runOpenCodeCloudDetail();
8120
8530
  continue;
8121
8531
  }
8122
8532
  if (typeof choice === "string" && choice.startsWith("provider:")) {
@@ -8291,12 +8701,12 @@ function translateResponsesTools(tools) {
8291
8701
  }
8292
8702
  return Object.keys(out).length ? out : void 0;
8293
8703
  }
8294
- function translateResponsesRequest(body, npm) {
8704
+ function translateResponsesRequest(body, npm, metadata) {
8295
8705
  const { system, messages } = translateResponsesInput(body.input, body.instructions, npm);
8296
8706
  const effort = body.reasoning?.effort;
8297
8707
  const providerOptions = deepMergeProviderOptions(
8298
8708
  thinkingProviderOptions(npm),
8299
- effortProviderOptions(npm, effort, body.model)
8709
+ effortProviderOptions(npm, effort, body.model, metadata)
8300
8710
  );
8301
8711
  return {
8302
8712
  system,
@@ -8338,11 +8748,9 @@ async function writeResponsesStream(fullStream, modelId, write) {
8338
8748
  let textItemId = null;
8339
8749
  let textOutputIndex = 0;
8340
8750
  let textFull = "";
8341
- let toolItemId = null;
8342
- let toolCallId = null;
8343
- let toolName = null;
8344
- let toolOutputIndex = 0;
8345
- let toolArgsFull = "";
8751
+ const toolStates = [];
8752
+ const toolStatesById = /* @__PURE__ */ new Map();
8753
+ let currentToolState = null;
8346
8754
  let reasoningItemId = null;
8347
8755
  let reasoningText = "";
8348
8756
  let reasoningOutputIndex = 0;
@@ -8367,6 +8775,51 @@ async function writeResponsesStream(fullStream, modelId, write) {
8367
8775
  }
8368
8776
  return textItemId;
8369
8777
  };
8778
+ const rememberToolState = (state) => {
8779
+ toolStates.push(state);
8780
+ toolStatesById.set(state.itemId, state);
8781
+ toolStatesById.set(state.callId, state);
8782
+ currentToolState = state;
8783
+ return state;
8784
+ };
8785
+ const createToolState = (rawId, name, signature) => {
8786
+ const itemId = rawId ?? newItemId("fc");
8787
+ const state = rememberToolState({
8788
+ itemId,
8789
+ callId: encodeToolUseId(itemId, signature),
8790
+ name: name ?? "unknown",
8791
+ outputIndex: outputIndex++,
8792
+ args: ""
8793
+ });
8794
+ emit("response.output_item.added", {
8795
+ type: "response.output_item.added",
8796
+ output_index: state.outputIndex,
8797
+ item: {
8798
+ type: "function_call",
8799
+ id: state.itemId,
8800
+ call_id: state.callId,
8801
+ name: state.name,
8802
+ arguments: "",
8803
+ status: "in_progress"
8804
+ }
8805
+ });
8806
+ return state;
8807
+ };
8808
+ const findToolState = (part) => {
8809
+ const key = part.id ?? part.toolCallId;
8810
+ if (key) return toolStatesById.get(key) ?? currentToolState;
8811
+ return currentToolState;
8812
+ };
8813
+ const appendToolArgs = (state, delta) => {
8814
+ if (!delta) return;
8815
+ state.args += delta;
8816
+ emit("response.function_call_arguments.delta", {
8817
+ type: "response.function_call_arguments.delta",
8818
+ item_id: state.itemId,
8819
+ output_index: state.outputIndex,
8820
+ delta
8821
+ });
8822
+ };
8370
8823
  for await (const part of fullStream) {
8371
8824
  switch (part.type) {
8372
8825
  case "reasoning-start":
@@ -8410,64 +8863,20 @@ async function writeResponsesStream(fullStream, modelId, write) {
8410
8863
  break;
8411
8864
  case "tool-input-start": {
8412
8865
  const sig = grabRoundTripSignature(part);
8413
- toolItemId = part.id ?? newItemId("fc");
8414
- toolCallId = encodeToolUseId(toolItemId, sig);
8415
- toolName = part.toolName ?? "unknown";
8416
- toolArgsFull = "";
8417
- toolOutputIndex = outputIndex;
8418
- outputIndex++;
8419
- emit("response.output_item.added", {
8420
- type: "response.output_item.added",
8421
- output_index: toolOutputIndex,
8422
- item: {
8423
- type: "function_call",
8424
- id: toolItemId,
8425
- call_id: toolCallId,
8426
- name: toolName,
8427
- arguments: "",
8428
- status: "in_progress"
8429
- }
8430
- });
8866
+ createToolState(part.id ?? part.toolCallId, part.toolName, sig);
8431
8867
  break;
8432
8868
  }
8433
- case "tool-input-delta":
8434
- toolArgsFull += part.delta ?? part.text ?? "";
8435
- if (toolItemId) {
8436
- emit("response.function_call_arguments.delta", {
8437
- type: "response.function_call_arguments.delta",
8438
- item_id: toolItemId,
8439
- output_index: toolOutputIndex,
8440
- delta: part.delta ?? part.text ?? ""
8441
- });
8442
- }
8869
+ case "tool-input-delta": {
8870
+ const state = findToolState(part);
8871
+ if (state) appendToolArgs(state, part.delta ?? part.text ?? "");
8443
8872
  break;
8873
+ }
8444
8874
  case "tool-call": {
8445
- if (!toolItemId) {
8446
- const sig = grabRoundTripSignature(part);
8447
- toolItemId = part.toolCallId ?? newItemId("fc");
8448
- toolCallId = encodeToolUseId(toolItemId, sig);
8449
- toolName = part.toolName ?? "unknown";
8450
- toolArgsFull = JSON.stringify(part.input ?? {});
8451
- toolOutputIndex = outputIndex;
8452
- outputIndex++;
8453
- emit("response.output_item.added", {
8454
- type: "response.output_item.added",
8455
- output_index: toolOutputIndex,
8456
- item: {
8457
- type: "function_call",
8458
- id: toolItemId,
8459
- call_id: toolCallId,
8460
- name: toolName,
8461
- arguments: "",
8462
- status: "in_progress"
8463
- }
8464
- });
8465
- emit("response.function_call_arguments.delta", {
8466
- type: "response.function_call_arguments.delta",
8467
- item_id: toolItemId,
8468
- output_index: toolOutputIndex,
8469
- delta: toolArgsFull
8470
- });
8875
+ const sig = grabRoundTripSignature(part);
8876
+ const key = part.toolCallId ?? part.id;
8877
+ const state = (key ? toolStatesById.get(key) : void 0) ?? createToolState(key, part.toolName, sig);
8878
+ if (!state.args) {
8879
+ appendToolArgs(state, JSON.stringify(part.input ?? {}));
8471
8880
  }
8472
8881
  break;
8473
8882
  }
@@ -8530,24 +8939,24 @@ async function writeResponsesStream(fullStream, modelId, write) {
8530
8939
  });
8531
8940
  outputItems.unshift(reasoningItem);
8532
8941
  }
8533
- if (toolItemId && toolCallId && toolName) {
8942
+ for (const tool3 of toolStates) {
8534
8943
  emit("response.function_call_arguments.done", {
8535
8944
  type: "response.function_call_arguments.done",
8536
- item_id: toolItemId,
8537
- output_index: toolOutputIndex,
8538
- arguments: toolArgsFull
8945
+ item_id: tool3.itemId,
8946
+ output_index: tool3.outputIndex,
8947
+ arguments: tool3.args
8539
8948
  });
8540
8949
  const fcItem = {
8541
8950
  type: "function_call",
8542
- id: toolItemId,
8543
- call_id: toolCallId,
8544
- name: toolName,
8545
- arguments: toolArgsFull,
8951
+ id: tool3.itemId,
8952
+ call_id: tool3.callId,
8953
+ name: tool3.name,
8954
+ arguments: tool3.args,
8546
8955
  status: "completed"
8547
8956
  };
8548
8957
  emit("response.output_item.done", {
8549
8958
  type: "response.output_item.done",
8550
- output_index: toolOutputIndex,
8959
+ output_index: tool3.outputIndex,
8551
8960
  item: fcItem
8552
8961
  });
8553
8962
  outputItems.push(fcItem);
@@ -8597,10 +9006,11 @@ async function generateResponsesResponse(model, params, modelId) {
8597
9006
  });
8598
9007
  }
8599
9008
  for (const tc of r.toolCalls) {
9009
+ const encodedId = encodeToolUseId(tc.toolCallId, grabRoundTripSignature(tc));
8600
9010
  output.push({
8601
9011
  type: "function_call",
8602
9012
  id: tc.toolCallId,
8603
- call_id: tc.toolCallId,
9013
+ call_id: encodedId,
8604
9014
  name: tc.toolName,
8605
9015
  arguments: JSON.stringify(tc.input ?? {}),
8606
9016
  status: "completed"
@@ -8748,7 +9158,8 @@ async function startCodexProxy(routes, options = {}) {
8748
9158
  modelId: route.upstreamModelId,
8749
9159
  apiKey: route.apiKey,
8750
9160
  baseURL: route.baseURL,
8751
- providerId: route.modelId
9161
+ providerId: route.modelId,
9162
+ vertex: route.vertex
8752
9163
  }));
8753
9164
  }
8754
9165
  return new Promise((resolve, reject2) => {
@@ -8831,7 +9242,14 @@ async function startCodexProxy(routes, options = {}) {
8831
9242
  try {
8832
9243
  const params = translateResponsesRequest(
8833
9244
  body,
8834
- route.npm
9245
+ route.npm,
9246
+ {
9247
+ providerId: route.providerId,
9248
+ apiBaseUrl: route.baseURL,
9249
+ supportedParameters: route.supportedParameters,
9250
+ reasoning: route.reasoning,
9251
+ interleavedReasoningField: route.interleavedReasoningField
9252
+ }
8835
9253
  );
8836
9254
  if (debug) {
8837
9255
  const effort = body.reasoning?.effort;
@@ -8992,13 +9410,8 @@ function isProcessAlive(pid) {
8992
9410
  return false;
8993
9411
  }
8994
9412
  }
8995
- function sessionAgeMs(lock) {
8996
- const started = Date.parse(lock.startedAt);
8997
- if (Number.isNaN(started)) return Infinity;
8998
- return Date.now() - started;
8999
- }
9000
9413
  function isConcurrentSession(lock) {
9001
- return isProcessAlive(lock.pid) && sessionAgeMs(lock) <= STALE_SESSION_MS;
9414
+ return isProcessAlive(lock.pid);
9002
9415
  }
9003
9416
  function restoreCodexOverlay(env = process.env) {
9004
9417
  const removed = [];
@@ -9041,13 +9454,16 @@ function checkSessionLock(isTty, env = process.env) {
9041
9454
  // src/codex/profile.ts
9042
9455
  var CODEX_LAUNCH_SANDBOX = "danger-full-access";
9043
9456
  function profileReasoningLine(effort) {
9044
- return effort ? `model_reasoning_effort = "${effort}"
9457
+ return effort ? `model_reasoning_effort = ${tomlString(effort)}
9045
9458
  ` : "";
9046
9459
  }
9047
9460
  function profileSandboxLine() {
9048
- return `sandbox = "${CODEX_LAUNCH_SANDBOX}"
9461
+ return `sandbox = ${tomlString(CODEX_LAUNCH_SANDBOX)}
9049
9462
  `;
9050
9463
  }
9464
+ function tomlString(value) {
9465
+ return JSON.stringify(value);
9466
+ }
9051
9467
  function buildCodexProfileToml(spec) {
9052
9468
  const { route, proxyPort, catalogPath, modelReasoningEffort } = spec;
9053
9469
  const model = route.modelId;
@@ -9056,26 +9472,26 @@ function buildCodexProfileToml(spec) {
9056
9472
  const envKey = codexProviderEnvKey(route.providerId);
9057
9473
  const baseUrl = route.baseURL ?? "https://api.openai.com/v1";
9058
9474
  return `# Generated by relay-ai \u2014 do not edit
9059
- ${profileSandboxLine()}model = "${model}"
9060
- model_provider = "${route.providerId}"
9061
- model_catalog_json = "${catalogPath}"
9475
+ ${profileSandboxLine()}model = ${tomlString(model)}
9476
+ model_provider = ${tomlString(route.providerId)}
9477
+ model_catalog_json = ${tomlString(catalogPath)}
9062
9478
  ${reasoning}
9063
9479
  [model_providers.${route.providerId}]
9064
- name = "${route.providerId}"
9065
- base_url = "${baseUrl}"
9066
- env_key = "${envKey}"
9480
+ name = ${tomlString(route.providerId)}
9481
+ base_url = ${tomlString(baseUrl)}
9482
+ env_key = ${tomlString(envKey)}
9067
9483
  wire_api = "responses"
9068
9484
  `;
9069
9485
  }
9070
9486
  const proxyBase = `http://127.0.0.1:${proxyPort}/v1`;
9071
9487
  return `# Generated by relay-ai \u2014 do not edit
9072
- ${profileSandboxLine()}model = "${model}"
9488
+ ${profileSandboxLine()}model = ${tomlString(model)}
9073
9489
  model_provider = "relay-ai-proxy"
9074
- model_catalog_json = "${catalogPath}"
9490
+ model_catalog_json = ${tomlString(catalogPath)}
9075
9491
  ${reasoning}
9076
9492
  [model_providers.relay-ai-proxy]
9077
9493
  name = "relay-ai"
9078
- base_url = "${proxyBase}"
9494
+ base_url = ${tomlString(proxyBase)}
9079
9495
  env_key = "RELAY_AI_CODEX_KEY"
9080
9496
  wire_api = "responses"
9081
9497
  `;
@@ -9340,7 +9756,13 @@ function buildEntry(r, priority) {
9340
9756
  }
9341
9757
  function defaultReasoningEffortForFavorite(r) {
9342
9758
  const model = enrichFavoriteModel(r);
9343
- const caps = getReasoningCapabilities(model.npm ?? "", model.upstreamModelId ?? model.id);
9759
+ const caps = getReasoningCapabilities(model.npm ?? "", model.upstreamModelId ?? model.id, {
9760
+ providerId: r.providerId,
9761
+ apiBaseUrl: model.apiBaseUrl,
9762
+ supportedParameters: model.supportedParameters,
9763
+ reasoning: model.reasoning,
9764
+ interleavedReasoningField: model.interleavedReasoningField
9765
+ });
9344
9766
  return caps.levels.length > 0 ? caps.defaultLevel : "none";
9345
9767
  }
9346
9768
  function buildFavoritesAppCatalog(resolved) {
@@ -9356,65 +9778,6 @@ function buildFavoritesAppCatalog(resolved) {
9356
9778
 
9357
9779
  // src/codex/favorites-launch.ts
9358
9780
  import * as p12 from "@clack/prompts";
9359
-
9360
- // src/favorites-resolver.ts
9361
- var ZEN_GO_PROVIDER_NAME = {
9362
- zen: "OpenCode Zen",
9363
- go: "OpenCode Go"
9364
- };
9365
- function resolveFavorite(fav, ctx) {
9366
- if (fav.providerId === "zen" || fav.providerId === "go") {
9367
- if (!ctx.zenGoApiKey) return void 0;
9368
- const models = fav.providerId === "zen" ? ctx.zenModels : ctx.goModels;
9369
- const model = models?.find((m) => m.id === fav.modelId);
9370
- if (!model) return void 0;
9371
- return {
9372
- providerId: fav.providerId,
9373
- providerName: ZEN_GO_PROVIDER_NAME[fav.providerId],
9374
- model,
9375
- apiKey: ctx.zenGoApiKey,
9376
- sourceBackend: fav.providerId
9377
- };
9378
- }
9379
- if (ctx.findLocalModel) {
9380
- const found = ctx.findLocalModel(fav.providerId, fav.modelId);
9381
- if (!found) return void 0;
9382
- if (ctx.agent && shouldHideModel({ providerId: fav.providerId, modelId: fav.modelId, agent: ctx.agent })) {
9383
- return void 0;
9384
- }
9385
- return {
9386
- providerId: fav.providerId,
9387
- providerName: found.provider.name,
9388
- model: found.model,
9389
- apiKey: found.provider.apiKey
9390
- };
9391
- }
9392
- return void 0;
9393
- }
9394
- function buildFavoritesList(starting, favorites, ctx, max = 20) {
9395
- const droppedFavorites = [];
9396
- const seen = /* @__PURE__ */ new Set();
9397
- const out = [];
9398
- if (starting) {
9399
- seen.add(`${starting.providerId}::${starting.model.id}`);
9400
- out.push(starting);
9401
- }
9402
- for (const fav of favorites) {
9403
- if (out.length >= max) break;
9404
- const key = `${fav.providerId}::${fav.modelId}`;
9405
- if (seen.has(key)) continue;
9406
- const resolved = resolveFavorite(fav, ctx);
9407
- if (!resolved) {
9408
- droppedFavorites.push(fav);
9409
- continue;
9410
- }
9411
- seen.add(key);
9412
- out.push(resolved);
9413
- }
9414
- return { resolved: out, droppedFavorites };
9415
- }
9416
-
9417
- // src/codex/favorites-launch.ts
9418
9781
  function buildCodexProxyRoutesFromResolved(resolved, providersById) {
9419
9782
  return resolved.map((r) => {
9420
9783
  const provider = providersById.get(r.providerId);
@@ -9608,13 +9971,21 @@ function codexHelpText() {
9608
9971
 
9609
9972
  ${pc13.bold("Usage:")}
9610
9973
  relay-ai codex [options] [codex-flags]
9974
+ relay-ai codex --vertex
9611
9975
  relay-ai codex --restore
9612
9976
  relay-ai codex --config
9977
+ relay-ai codex --help
9978
+ relay-ai codex --version
9613
9979
 
9614
9980
  ${pc13.bold("Options:")}
9615
9981
  --trace Write proxy debug logs to ~/.relay-ai/logs/ and show errors on exit
9616
9982
  --provider Boot provider id (skip wizard when paired with --model or non-interactive)
9617
9983
  --model Boot model id (skip wizard when paired with --provider or non-interactive)
9984
+ --vertex Use Claude models through Google Vertex AI
9985
+ --restore Remove interrupted-session overlay files
9986
+ --config Preview/write launch configuration without starting Codex
9987
+ --help Show this command help
9988
+ --version Show version
9618
9989
 
9619
9990
  ${pc13.bold("Description:")}
9620
9991
  Picks a provider and model from ~/.relay-ai/providers.json, writes a temporary
@@ -9650,15 +10021,26 @@ ${pc13.bold("Examples:")}
9650
10021
  ${pc13.bold("Favorites:")}
9651
10022
  When you have saved favorites via ${pc13.cyan("relay-ai models")}, the Codex
9652
10023
  picker will show your starting model + favorites for mid-session switching.
9653
- Zen/Go favorites are skipped \u2014 use ${pc13.cyan("relay-ai claude")} or
9654
- ${pc13.cyan("relay-ai server")} for those.`;
10024
+ Zen/Go favorites are included when an OpenCode API key is available.`;
9655
10025
  }
9656
10026
  async function writeLaunchArtifacts(route, selectedModel, providerName, proxyPort) {
9657
10027
  const catalogPath = getCatalogOutputPath(route.providerId);
9658
10028
  const catalog = buildCatalogFile([selectedModel], providerName);
9659
10029
  writeOverlayFile(catalogPath, serializeCatalog(catalog));
9660
10030
  const profilePath = getProfileOutputPath();
9661
- writeOverlayFile(profilePath, buildCodexProfileToml({ route, proxyPort, catalogPath }));
10031
+ const caps = getReasoningCapabilities(route.npm, route.upstreamModelId, {
10032
+ providerId: route.providerId,
10033
+ apiBaseUrl: route.baseURL,
10034
+ supportedParameters: route.supportedParameters,
10035
+ reasoning: route.reasoning,
10036
+ interleavedReasoningField: route.interleavedReasoningField
10037
+ });
10038
+ writeOverlayFile(profilePath, buildCodexProfileToml({
10039
+ route,
10040
+ proxyPort,
10041
+ catalogPath,
10042
+ modelReasoningEffort: caps.defaultLevel || void 0
10043
+ }));
9662
10044
  return { profilePath, catalogPath };
9663
10045
  }
9664
10046
  async function writeFavoritesLaunchArtifacts(resolved, starting, proxyPort) {
@@ -9696,6 +10078,103 @@ function printCodexCleanupReminder(hadProxy) {
9696
10078
  parts.push("If a future session acts stuck: relay-ai codex --restore");
9697
10079
  p13.log.info(parts.join(" "));
9698
10080
  }
10081
+ function vertexEntryToLocalModel(entry) {
10082
+ return {
10083
+ id: entry.id,
10084
+ name: entry.display_name,
10085
+ family: "claude",
10086
+ brand: "Anthropic",
10087
+ modelFormat: "openai",
10088
+ upstreamModelId: entry.upstream_id ?? entry.id,
10089
+ baseUrl: "",
10090
+ npm: VERTEX_ANTHROPIC_NPM,
10091
+ contextWindow: resolveContextWindow(entry.id)
10092
+ };
10093
+ }
10094
+ async function runCodexVertexLaunch(passthroughArgs, trace) {
10095
+ if (!hasApplicationDefaultCredentials()) {
10096
+ p13.log.error("Google Application Default Credentials not found.");
10097
+ p13.log.info("Run: gcloud auth application-default login");
10098
+ return 1;
10099
+ }
10100
+ const config = buildVertexRuntimeConfig();
10101
+ if (!config) {
10102
+ p13.log.error("ANTHROPIC_VERTEX_PROJECT_ID (or GOOGLE_CLOUD_PROJECT) is not set.");
10103
+ p13.log.info("Set your project: export ANTHROPIC_VERTEX_PROJECT_ID=your-project-id");
10104
+ return 1;
10105
+ }
10106
+ let selectedEntry;
10107
+ if (config.models.length === 1) {
10108
+ selectedEntry = config.models[0];
10109
+ } else {
10110
+ const choice = await p13.select({
10111
+ message: "Select a Vertex AI model:",
10112
+ options: config.models.map((m) => ({ value: m, label: m.display_name, hint: m.id }))
10113
+ });
10114
+ if (p13.isCancel(choice)) {
10115
+ p13.cancel("Cancelled.");
10116
+ return 0;
10117
+ }
10118
+ selectedEntry = choice;
10119
+ }
10120
+ process.env["ANTHROPIC_VERTEX_PROJECT_ID"] = config.project;
10121
+ process.env["GOOGLE_CLOUD_LOCATION"] = config.location;
10122
+ const vertexConfig = { project: config.project, location: config.location };
10123
+ const allModels = config.models.map(vertexEntryToLocalModel);
10124
+ const allRoutes = allModels.map((m) => ({
10125
+ modelId: m.id,
10126
+ upstreamModelId: m.upstreamModelId,
10127
+ npm: VERTEX_ANTHROPIC_NPM,
10128
+ apiKey: "",
10129
+ providerId: "vertex",
10130
+ vertex: vertexConfig
10131
+ }));
10132
+ const startingRoute = {
10133
+ tier: "proxy",
10134
+ modelId: selectedEntry.id,
10135
+ upstreamModelId: selectedEntry.upstream_id ?? selectedEntry.id,
10136
+ npm: VERTEX_ANTHROPIC_NPM,
10137
+ apiKey: "",
10138
+ providerId: "vertex"
10139
+ };
10140
+ const debugLogPath = getCodexProxyDebugLogPath();
10141
+ let proxyHandle = null;
10142
+ try {
10143
+ p13.log.info(`Vertex AI \xB7 ${selectedEntry.display_name} \u2014 project: ${config.project} / location: ${config.location}`);
10144
+ proxyHandle = await startCodexProxy(allRoutes, { debug: trace });
10145
+ const proxyPort = proxyHandle.port;
10146
+ const catalogPath = getCatalogOutputPath("vertex");
10147
+ writeOverlayFile(catalogPath, serializeCatalog(buildCatalogFile(allModels, "Vertex AI")));
10148
+ const profilePath = getProfileOutputPath();
10149
+ const caps = getReasoningCapabilities(VERTEX_ANTHROPIC_NPM, selectedEntry.id);
10150
+ writeOverlayFile(profilePath, buildCodexProfileToml({
10151
+ route: startingRoute,
10152
+ proxyPort,
10153
+ catalogPath,
10154
+ modelReasoningEffort: caps.defaultLevel || void 0
10155
+ }));
10156
+ writeSessionLock({
10157
+ pid: process.pid,
10158
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
10159
+ profilePath,
10160
+ catalogPaths: [catalogPath],
10161
+ proxyPort
10162
+ });
10163
+ if (!isAgentStdoutMode()) {
10164
+ logProxy(proxyPort);
10165
+ logActiveModel(selectedEntry.display_name, selectedEntry.id);
10166
+ printCodexCliCleanupPanel("relay-ai codex --restore");
10167
+ }
10168
+ const childEnv = buildCodexChildEnv(startingRoute, proxyPort);
10169
+ const exitCode = await launchCodex(selectedEntry.id, childEnv, passthroughArgs);
10170
+ if (trace) printTraceLog(debugLogPath);
10171
+ printCodexCleanupReminder(true);
10172
+ return exitCode;
10173
+ } finally {
10174
+ proxyHandle?.close();
10175
+ restoreCodexOverlay();
10176
+ }
10177
+ }
9699
10178
  async function runCodexCommand(codexArgs, trace = false, launch = {}) {
9700
10179
  if (codexArgs.includes("--help") || codexArgs.includes("-h")) {
9701
10180
  console.log(codexHelpText());
@@ -9727,6 +10206,21 @@ async function runCodexCommand(codexArgs, trace = false, launch = {}) {
9727
10206
  p13.log.info(`Debug log: ${debugLogPath}`);
9728
10207
  }
9729
10208
  const isTty = Boolean(process.stdin.isTTY);
10209
+ if (launch.vertex) {
10210
+ if (!configOnly) {
10211
+ const sessionCheck = checkSessionLock(isTty);
10212
+ if (!sessionCheck.ok) {
10213
+ if (sessionCheck.reason === "non_tty") {
10214
+ console.error(pc13.red("relay-ai codex --vertex requires an interactive terminal."));
10215
+ return 1;
10216
+ }
10217
+ console.error(pc13.yellow(`Another relay-ai codex session may be running (pid ${sessionCheck.lock.pid}).`));
10218
+ console.error("Run relay-ai codex --restore to clean up, or wait for it to finish.");
10219
+ return 1;
10220
+ }
10221
+ }
10222
+ return runCodexVertexLaunch(passthroughArgs, trace);
10223
+ }
9730
10224
  const prefs = loadPreferences();
9731
10225
  const launchPlan = planLaunchWizard({
9732
10226
  explicit: { providerId: launch.launchProvider, modelId: launch.launchModel },
@@ -9821,9 +10315,17 @@ Error: ${launchPlan.error}
9821
10315
  const pickedProvider = await pickCodexProvider(compatible, prefs, favoritesActive);
9822
10316
  if (!pickedProvider) return 0;
9823
10317
  if (pickedProvider === "__favorites__") {
9824
- const fav = favorites[0];
9825
- activeProvider = compatible.find((p19) => p19.id === fav.providerId);
9826
- selectedModel = activeProvider.models.find((m) => m.id === fav.modelId);
10318
+ const favoriteProviders = compatible.map((provider) => ({
10319
+ ...provider,
10320
+ models: routableModelsForProvider(provider, "codex")
10321
+ }));
10322
+ const favoriteStart = resolveFirstAvailableFavorite(favorites, favoriteProviders);
10323
+ if (!favoriteStart) {
10324
+ p13.log.warn("No saved Codex favorites are currently available.");
10325
+ return 0;
10326
+ }
10327
+ activeProvider = favoriteStart.provider;
10328
+ selectedModel = favoriteStart.model;
9827
10329
  p13.log.step(`Loaded Favorites Catalog. Starting model: ${selectedModel.name || selectedModel.id} (${activeProvider.name})`);
9828
10330
  } else {
9829
10331
  activeProvider = pickedProvider;
@@ -9895,7 +10397,11 @@ Error: ${launchPlan.error}
9895
10397
  npm: route.npm,
9896
10398
  apiKey: route.apiKey,
9897
10399
  baseURL: route.baseURL,
9898
- upstreamModelId: route.upstreamModelId
10400
+ upstreamModelId: route.upstreamModelId,
10401
+ providerId: route.providerId,
10402
+ supportedParameters: route.supportedParameters,
10403
+ reasoning: route.reasoning,
10404
+ interleavedReasoningField: route.interleavedReasoningField
9899
10405
  }], { debug: trace });
9900
10406
  proxyPort = proxyHandle.port;
9901
10407
  }
@@ -10053,7 +10559,13 @@ function mergeAppConfig(existing, spec) {
10053
10559
  };
10054
10560
  const existingEffort = typeof out.model_reasoning_effort === "string" ? out.model_reasoning_effort : void 0;
10055
10561
  if (existingEffort !== void 0) {
10056
- const caps = getReasoningCapabilities(spec.route.npm, spec.route.modelId);
10562
+ const caps = getReasoningCapabilities(spec.route.npm, spec.route.modelId, {
10563
+ providerId: spec.route.providerId,
10564
+ apiBaseUrl: spec.route.baseURL,
10565
+ supportedParameters: spec.route.supportedParameters,
10566
+ reasoning: spec.route.reasoning,
10567
+ interleavedReasoningField: spec.route.interleavedReasoningField
10568
+ });
10057
10569
  if (caps.levels.length === 0 || !caps.levels.includes(existingEffort)) {
10058
10570
  if (caps.levels.length > 0 && caps.defaultLevel) {
10059
10571
  out.model_reasoning_effort = caps.defaultLevel;
@@ -10243,7 +10755,7 @@ function removeAppCatalogs(env = process.env) {
10243
10755
  }
10244
10756
  function restoreCodexAppOverlay(env = process.env) {
10245
10757
  const lock = readAppSessionLock(env);
10246
- if (lock && isConcurrentSession(lock)) {
10758
+ if (lock && lock.pid !== process.pid && isConcurrentSession(lock)) {
10247
10759
  return {
10248
10760
  restored: false,
10249
10761
  liveSession: true,
@@ -10272,7 +10784,7 @@ function recoverInterruptedCodexAppSession(env = process.env) {
10272
10784
  const lock = readAppSessionLock(env);
10273
10785
  const managed = isAppManagedConfig(readCodexConfigText());
10274
10786
  if (!lock && !managed) return { recovered: false };
10275
- if (lock && isConcurrentSession(lock)) {
10787
+ if (lock && lock.pid !== process.pid && isConcurrentSession(lock)) {
10276
10788
  return { recovered: false };
10277
10789
  }
10278
10790
  restoreCodexAppOverlay(env);
@@ -10281,7 +10793,7 @@ function recoverInterruptedCodexAppSession(env = process.env) {
10281
10793
  function checkAppSessionLock(isTty, env = process.env) {
10282
10794
  if (!isTty) return { ok: false, reason: "non_tty" };
10283
10795
  const lock = readAppSessionLock(env);
10284
- if (lock && isConcurrentSession(lock)) {
10796
+ if (lock && lock.pid !== process.pid && isConcurrentSession(lock)) {
10285
10797
  return { ok: false, reason: "concurrent", lock };
10286
10798
  }
10287
10799
  return { ok: true };
@@ -10519,9 +11031,19 @@ function codexAppHelpText() {
10519
11031
  return `${pc14.bold("relay-ai codex-app")} \u2014 launch Codex desktop app with your registry providers
10520
11032
 
10521
11033
  ${pc14.bold("Usage:")}
10522
- relay-ai codex-app
11034
+ relay-ai codex-app [options]
11035
+ relay-ai codex-app --vertex
10523
11036
  relay-ai codex-app --restore
10524
11037
  relay-ai codex-app --config
11038
+ relay-ai codex-app --help
11039
+ relay-ai codex-app --version
11040
+
11041
+ ${pc14.bold("Options:")}
11042
+ --vertex Use Claude models through Google Vertex AI
11043
+ --restore Restore Codex config after an interrupted app session
11044
+ --config Preview the generated Codex app configuration without launching
11045
+ --help Show this command help
11046
+ --version Show version
10525
11047
 
10526
11048
  ${pc14.bold("Description:")}
10527
11049
  Picks a provider and model from ~/.relay-ai/providers.json, patches ~/.codex/config.toml
@@ -10542,18 +11064,161 @@ ${pc14.bold("Preview (no writes):")}
10542
11064
 
10543
11065
  ${pc14.bold("Examples:")}
10544
11066
  relay-ai codex-app
11067
+ relay-ai codex-app --vertex
11068
+ relay-ai codex-app --config
10545
11069
  relay-ai codex-app --restore
10546
11070
 
10547
11071
  ${pc14.bold("Favorites:")}
10548
11072
  When you have saved favorites via ${pc14.cyan("relay-ai models")}, the Codex App
10549
11073
  picker will show your starting model + favorites for mid-session switching.
10550
- Zen/Go favorites are skipped \u2014 use ${pc14.cyan("relay-ai claude")} or
10551
- ${pc14.cyan("relay-ai server")} for those.`;
11074
+ Zen/Go favorites are included when an OpenCode API key is available.`;
10552
11075
  }
10553
11076
  function providerForCodexPicker(provider) {
10554
11077
  return { ...provider, models: routableModelsForProvider(provider, "codex-app") };
10555
11078
  }
10556
- async function runCodexAppCommand(args) {
11079
+ function vertexEntryToLocalModel2(entry) {
11080
+ return {
11081
+ id: entry.id,
11082
+ name: entry.display_name,
11083
+ family: "claude",
11084
+ brand: "Anthropic",
11085
+ modelFormat: "openai",
11086
+ upstreamModelId: entry.upstream_id ?? entry.id,
11087
+ baseUrl: "",
11088
+ npm: VERTEX_ANTHROPIC_NPM,
11089
+ contextWindow: resolveContextWindow(entry.id)
11090
+ };
11091
+ }
11092
+ async function runCodexAppVertexLaunch(configOnly) {
11093
+ if (!hasApplicationDefaultCredentials()) {
11094
+ p15.log.error("Google Application Default Credentials not found.");
11095
+ p15.log.info("Run: gcloud auth application-default login");
11096
+ return 1;
11097
+ }
11098
+ const config = buildVertexRuntimeConfig();
11099
+ if (!config) {
11100
+ p15.log.error("ANTHROPIC_VERTEX_PROJECT_ID (or GOOGLE_CLOUD_PROJECT) is not set.");
11101
+ p15.log.info("Set your project: export ANTHROPIC_VERTEX_PROJECT_ID=your-project-id");
11102
+ return 1;
11103
+ }
11104
+ let selectedEntry;
11105
+ if (config.models.length === 1) {
11106
+ selectedEntry = config.models[0];
11107
+ } else {
11108
+ const choice = await p15.select({
11109
+ message: "Select a starting Vertex AI model:",
11110
+ options: config.models.map((m) => ({ value: m, label: m.display_name, hint: m.id }))
11111
+ });
11112
+ if (p15.isCancel(choice)) {
11113
+ p15.cancel("Cancelled.");
11114
+ return 0;
11115
+ }
11116
+ selectedEntry = choice;
11117
+ }
11118
+ process.env["ANTHROPIC_VERTEX_PROJECT_ID"] = config.project;
11119
+ process.env["GOOGLE_CLOUD_LOCATION"] = config.location;
11120
+ const vertexConfig = { project: config.project, location: config.location };
11121
+ const vertexModels = config.models.map(vertexEntryToLocalModel2);
11122
+ const catalogPath = getAppCatalogPath("vertex");
11123
+ const route = {
11124
+ tier: "proxy",
11125
+ modelId: selectedEntry.id,
11126
+ upstreamModelId: selectedEntry.upstream_id ?? selectedEntry.id,
11127
+ npm: VERTEX_ANTHROPIC_NPM,
11128
+ apiKey: "",
11129
+ providerId: "vertex"
11130
+ };
11131
+ if (configOnly) {
11132
+ const home = process.env["HOME"] ?? "";
11133
+ const shortenPath = (fp) => home ? fp.replace(home, "~") : fp;
11134
+ console.log("");
11135
+ console.log(pc14.bold(pc14.cyan(" CONFIG PREVIEW \u2014 relay-ai codex-app --vertex")));
11136
+ console.log("");
11137
+ console.log(` ${pc14.bold("Mode:")} Vertex AI`);
11138
+ console.log(` ${pc14.bold("Project:")} ${config.project}`);
11139
+ console.log(` ${pc14.bold("Location:")} ${config.location}`);
11140
+ console.log(` ${pc14.bold("Model:")} ${selectedEntry.display_name}`);
11141
+ console.log(` ${pc14.bold("Catalog:")} ${vertexModels.length} model${vertexModels.length !== 1 ? "s" : ""} available`);
11142
+ console.log("");
11143
+ console.log(` ${pc14.bold("Catalog file:")}`);
11144
+ console.log(` ${pc14.dim(shortenPath(catalogPath))}`);
11145
+ console.log("");
11146
+ console.log(pc14.dim(" No app was launched."));
11147
+ console.log(pc14.dim(" Run ") + pc14.cyan("relay-ai codex-app --vertex") + pc14.dim(" to launch."));
11148
+ console.log("");
11149
+ return 0;
11150
+ }
11151
+ let proxyHandle = null;
11152
+ let sessionActive = false;
11153
+ try {
11154
+ proxyHandle = await startCodexProxy(
11155
+ vertexModels.map((m) => ({
11156
+ modelId: m.id,
11157
+ upstreamModelId: m.upstreamModelId,
11158
+ npm: VERTEX_ANTHROPIC_NPM,
11159
+ apiKey: "",
11160
+ providerId: "vertex",
11161
+ vertex: vertexConfig
11162
+ })),
11163
+ { requireAuth: false }
11164
+ );
11165
+ const proxyPort = proxyHandle.port;
11166
+ const catalogFile = buildAppCatalogFile(vertexModels, "Vertex AI", selectedEntry.id);
11167
+ writeOverlayFile(catalogPath, serializeCatalog(catalogFile));
11168
+ const spec = {
11169
+ route,
11170
+ proxyPort,
11171
+ catalogPath,
11172
+ providerDisplayName: `${selectedEntry.display_name} \xB7 Vertex AI`
11173
+ };
11174
+ saveAppRestoreStateBeforePatch();
11175
+ const backupPath = backupConfigToml();
11176
+ applyAppConfigPatch(spec);
11177
+ writeAppSessionLock({
11178
+ pid: process.pid,
11179
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
11180
+ configPath: getCodexConfigPath(),
11181
+ catalogPaths: [catalogPath],
11182
+ restoreStatePath: getAppRestoreStatePath(),
11183
+ backupPath,
11184
+ proxyPort
11185
+ });
11186
+ sessionActive = true;
11187
+ p15.log.info(`Vertex AI \xB7 ${selectedEntry.display_name} \u2014 project: ${config.project} / location: ${config.location}`);
11188
+ logProxy(proxyPort);
11189
+ logActiveModel(selectedEntry.display_name, selectedEntry.id);
11190
+ try {
11191
+ await launchOrRestartCodexApp();
11192
+ } catch (err) {
11193
+ p15.log.warn(String(err instanceof Error ? err.message : err));
11194
+ p15.log.info(codexAppInstallHint());
11195
+ }
11196
+ printCodexAppSessionPanel({
11197
+ modelLabel: selectedEntry.display_name,
11198
+ modelId: selectedEntry.id,
11199
+ providerName: "Vertex AI",
11200
+ restoreCommand: "relay-ai codex-app --restore"
11201
+ });
11202
+ codexAppOutro(selectedEntry.display_name);
11203
+ await waitForShutdown2();
11204
+ console.log("");
11205
+ if (sessionActive) {
11206
+ restoreCodexAppOverlay();
11207
+ sessionActive = false;
11208
+ }
11209
+ if (isCodexAppRunning()) {
11210
+ const shouldClose = await p15.confirm({ message: "Codex Desktop is still running. Close it?" });
11211
+ if (shouldClose && !p15.isCancel(shouldClose)) {
11212
+ quitCodexAppGracefully();
11213
+ }
11214
+ }
11215
+ return 0;
11216
+ } finally {
11217
+ proxyHandle?.close();
11218
+ if (sessionActive) restoreCodexAppOverlay();
11219
+ }
11220
+ }
11221
+ async function runCodexAppCommand(args, opts = {}) {
10557
11222
  if (args.includes("--help") || args.includes("-h")) {
10558
11223
  console.log(codexAppHelpText());
10559
11224
  return 0;
@@ -10590,6 +11255,9 @@ async function runCodexAppCommand(args) {
10590
11255
  p15.log.warn("Recovered from an interrupted codex-app session (restored Codex config).");
10591
11256
  }
10592
11257
  }
11258
+ if (opts.vertex) {
11259
+ return runCodexAppVertexLaunch(configOnly);
11260
+ }
10593
11261
  const catalogSpinner = p15.spinner();
10594
11262
  catalogSpinner.start("Loading your providers...");
10595
11263
  let catalog;
@@ -10626,9 +11294,14 @@ async function runCodexAppCommand(args) {
10626
11294
  const pickedProvider = await pickCodexProvider(compatible, prefs, favoritesActive);
10627
11295
  if (!pickedProvider) return 0;
10628
11296
  if (pickedProvider === "__favorites__") {
10629
- const fav = favorites[0];
10630
- activeProvider = providerForCodexPicker(compatible.find((p19) => p19.id === fav.providerId));
10631
- selectedModel = activeProvider.models.find((m) => m.id === fav.modelId);
11297
+ const favoriteProviders = compatible.map(providerForCodexPicker);
11298
+ const favoriteStart = resolveFirstAvailableFavorite(favorites, favoriteProviders);
11299
+ if (!favoriteStart) {
11300
+ p15.log.warn("No saved Codex App favorites are currently available.");
11301
+ return 0;
11302
+ }
11303
+ activeProvider = favoriteStart.provider;
11304
+ selectedModel = favoriteStart.model;
10632
11305
  p15.log.step(`Loaded Favorites Catalog. Starting model: ${selectedModel.name || selectedModel.id} (${activeProvider.name})`);
10633
11306
  } else {
10634
11307
  activeProvider = providerForCodexPicker(pickedProvider);
@@ -11202,7 +11875,17 @@ function claudeAppHelpText() {
11202
11875
  return `${pc15.bold("relay-ai claude-app")} \u2014 launch Claude Desktop app in 3P mode with your registry providers
11203
11876
 
11204
11877
  ${pc15.bold("Usage:")}
11205
- relay-ai claude-app
11878
+ relay-ai claude-app [options]
11879
+ relay-ai claude-app --trace
11880
+ relay-ai claude-app --restore
11881
+ relay-ai claude-app --help
11882
+ relay-ai claude-app --version
11883
+
11884
+ ${pc15.bold("Options:")}
11885
+ --trace Write proxy debug logs to ~/.relay-ai/logs/
11886
+ --restore Restore Claude Desktop config after an interrupted app session
11887
+ --help Show this command help
11888
+ --version Show version
11206
11889
 
11207
11890
  ${pc15.bold("Description:")}
11208
11891
  Picks a provider and model from ~/.relay-ai/providers.json, patches Claude Desktop config
@@ -11214,6 +11897,7 @@ ${pc15.bold("Platforms:")}
11214
11897
 
11215
11898
  ${pc15.bold("Cleanup:")}
11216
11899
  Ctrl+C stops the proxy and restores your previous Claude config.
11900
+ After a crash: relay-ai claude-app --restore
11217
11901
  `;
11218
11902
  }
11219
11903
  function providerForClaudePicker(provider) {
@@ -11224,6 +11908,14 @@ async function runClaudeAppCommand(args) {
11224
11908
  console.log(claudeAppHelpText());
11225
11909
  return 0;
11226
11910
  }
11911
+ if (args.includes("--restore")) {
11912
+ recoverSession();
11913
+ console.log("Restored Claude Desktop relay-ai config.");
11914
+ return 0;
11915
+ }
11916
+ const trace = args.includes("--trace");
11917
+ const debugLogPath = trace ? getProxyDebugLogPath() : void 0;
11918
+ if (trace) console.log(`Debug log: ${debugLogPath}`);
11227
11919
  try {
11228
11920
  claudeAppSupported();
11229
11921
  } catch (err) {
@@ -11321,7 +12013,8 @@ async function runClaudeAppCommand(args) {
11321
12013
  serverPassword: null,
11322
12014
  catalog: createGatewayModelCatalog(serverModels, { maskGatewayIds: true }),
11323
12015
  backends: BACKENDS,
11324
- gateway: { maskGatewayIds: true }
12016
+ gateway: { maskGatewayIds: true },
12017
+ debugLogPath
11325
12018
  });
11326
12019
  const uuid = writeRelayAiConfig(proxyHandle.port);
11327
12020
  writeSessionLock2({
@@ -11792,7 +12485,7 @@ PROVIDER / MODEL DISCOVERY FOR ALEF CONFIG
11792
12485
  5. relay-ai --ai (includes live state section at bottom of output)
11793
12486
 
11794
12487
  ALEF CHECKLIST
11795
- \u25A1 relay-ai on PATH (npm install -g relay-ai; dev: npm link after builds)
12488
+ \u25A1 relay-ai on PATH (npm install -g @jacobbd/relay-ai; dev: npm link after builds)
11796
12489
  \u25A1 Always pass --provider + --model (or provider__model slug) \u2014 never rely on wizard
11797
12490
  \u25A1 Claude: --output-format stream-json (or json) with -p
11798
12491
  \u25A1 Codex: exec --json (not bare codex exec without --json if parsing stdout)
@@ -11998,6 +12691,7 @@ function parseArgs(args) {
11998
12691
  for (const arg of rest) {
11999
12692
  if (arg === "--help" || arg === "-h") parsed2.showHelp = true;
12000
12693
  else if (arg === "--version" || arg === "-v") parsed2.showVersion = true;
12694
+ else if (arg === "--vertex") parsed2.vertex = true;
12001
12695
  }
12002
12696
  return parsed2;
12003
12697
  }
@@ -12018,6 +12712,10 @@ function parseArgs(args) {
12018
12712
  parsed2.trace = true;
12019
12713
  continue;
12020
12714
  }
12715
+ if (arg === "--vertex") {
12716
+ parsed2.vertex = true;
12717
+ continue;
12718
+ }
12021
12719
  if (arg === "--help" || arg === "-h") {
12022
12720
  parsed2.showHelp = true;
12023
12721
  continue;
@@ -12069,28 +12767,37 @@ function parseArgs(args) {
12069
12767
  }
12070
12768
  function rootHelpText() {
12071
12769
  return `${pc16.bold("relay-ai")} v${VERSION}
12072
- Launch AI coding tools with OpenCode Zen, Go, or local providers (Groq, Mistral,
12770
+ Launch AI coding tools with OpenCode Zen / Go or local providers (Groq, Mistral,
12073
12771
  OpenAI, Gemini, Ollama, and more).
12074
12772
 
12075
12773
  ${pc16.bold("Usage:")}
12076
12774
  relay-ai claude [options] [claude-flags]
12775
+ relay-ai claude-app [options]
12776
+ relay-ai codex [options] [codex-flags]
12777
+ relay-ai codex-app [options]
12778
+ relay-ai server [options]
12077
12779
  relay-ai models
12780
+ relay-ai favorites
12078
12781
  relay-ai providers
12079
- relay-ai codex [codex-flags]
12080
- relay-ai codex-app
12081
- relay-ai server
12082
12782
  relay-ai --help
12083
12783
  relay-ai --version
12084
12784
  relay-ai --ai Full reference for AI agents (run this when unsure)
12085
12785
  relay-ai --ai --install Install or upgrade agent skill when version changed
12086
12786
  relay-ai --ai --install --force Reinstall skill even if already current
12087
12787
 
12788
+ ${pc16.bold("Root options:")}
12789
+ -h, --help Show this help
12790
+ -v, --version Show version
12791
+ --ai Print the full reference for AI agents
12792
+ --ai --install Install or upgrade the relay-ai agent skill
12793
+ --force Reinstall the agent skill when used with --ai --install
12794
+
12088
12795
  ${pc16.bold("Commands:")}
12089
12796
  claude Launch Claude Code \u2014 pick a provider from your registry
12090
12797
  models Manage favorite models for mid-session /model switching (max ${MAX_MODEL_CATALOG})
12091
12798
  favorites Alias for models
12092
12799
  providers Add, import, and manage your AI providers
12093
- server Run a foreground API gateway (Zen, Go, and local providers)
12800
+ server Run a foreground API gateway (OpenCode Zen / Go and local providers)
12094
12801
  codex Launch OpenAI Codex CLI with registry providers
12095
12802
  codex-app Launch Codex desktop app with registry providers (macOS + Windows)
12096
12803
  claude-app Launch Claude Desktop app with registry providers (macOS + Windows)
@@ -12102,8 +12809,10 @@ ${pc16.bold("Migration:")}
12102
12809
  ${pc16.bold("Examples:")}
12103
12810
  relay-ai claude
12104
12811
  relay-ai models
12812
+ relay-ai providers
12105
12813
  relay-ai codex
12106
12814
  relay-ai codex-app
12815
+ relay-ai claude-app
12107
12816
  relay-ai server
12108
12817
  relay-ai claude -c
12109
12818
  relay-ai claude --resume abc-123
@@ -12175,7 +12884,7 @@ ${pc16.bold("Behavior:")}
12175
12884
  ${pc16.bold("Vertex env:")}
12176
12885
  ANTHROPIC_VERTEX_PROJECT_ID or GOOGLE_CLOUD_PROJECT \u2014 your GCP project
12177
12886
  GOOGLE_CLOUD_LOCATION or CLOUD_ML_REGION \u2014 region (default: global)
12178
- Optional catalog: ~/.relay-ai/vertex-models.json (see vertex-models.example.json)
12887
+ Optional catalog: ~/.relay-ai/vertex-models.json (see assets/vertex-models.example.json)
12179
12888
 
12180
12889
  ${pc16.bold("Endpoints:")}
12181
12890
  Anthropic-compatible: ANTHROPIC_BASE_URL=http://127.0.0.1:17645/anthropic
@@ -12461,9 +13170,13 @@ Error: ${launchPlan.error}
12461
13170
  }
12462
13171
  const providerChoice = chosen;
12463
13172
  if (providerChoice === "__favorites__") {
12464
- const fav = favorites[0];
12465
- activeProvider = allProviders.find((p19) => p19.id === fav.providerId);
12466
- selectedModel = activeProvider.models.find((m) => m.id === fav.modelId);
13173
+ const favoriteStart = resolveFirstAvailableFavorite(favorites, allProviders);
13174
+ if (!favoriteStart) {
13175
+ p18.log.warn("No saved favorites are currently available.");
13176
+ return 0;
13177
+ }
13178
+ activeProvider = favoriteStart.provider;
13179
+ selectedModel = favoriteStart.model;
12467
13180
  p18.log.step(`Loaded Favorites Catalog. Starting model: ${selectedModel.name || selectedModel.id} (${activeProvider.name})`);
12468
13181
  } else {
12469
13182
  activeProvider = allProviders.find((lp) => lp.id === providerChoice);
@@ -12559,7 +13272,11 @@ Error: ${launchPlan.error}
12559
13272
  {
12560
13273
  npm: selectedModel.npm,
12561
13274
  baseURL: selectedModel.apiBaseUrl,
12562
- upstreamModelId: selectedModel.upstreamModelId
13275
+ upstreamModelId: selectedModel.upstreamModelId,
13276
+ providerId: activeProvider.id,
13277
+ supportedParameters: selectedModel.supportedParameters,
13278
+ reasoning: selectedModel.reasoning,
13279
+ interleavedReasoningField: selectedModel.interleavedReasoningField
12563
13280
  }
12564
13281
  );
12565
13282
  if (!isAgentStdoutMode()) {
@@ -12659,7 +13376,7 @@ Error: ${parsed.error}
12659
13376
  console.log(VERSION);
12660
13377
  return 0;
12661
13378
  }
12662
- return runCodexAppCommand(parsed.claudeArgs);
13379
+ return runCodexAppCommand(parsed.claudeArgs, { vertex: parsed.vertex });
12663
13380
  }
12664
13381
  if (parsed.command === "claude-app") {
12665
13382
  if (parsed.showVersion) {
@@ -12679,7 +13396,8 @@ Error: ${parsed.error}
12679
13396
  }
12680
13397
  return runCodexCommand(parsed.claudeArgs, parsed.trace, {
12681
13398
  launchProvider: parsed.launchProvider,
12682
- launchModel: parsed.launchModel
13399
+ launchModel: parsed.launchModel,
13400
+ vertex: parsed.vertex
12683
13401
  });
12684
13402
  }
12685
13403
  if (parsed.showVersion) {