@jacobbd/relay-ai 0.2.3 → 0.2.5

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.5";
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
  }
@@ -1576,14 +1713,19 @@ function resolveBaseURL(model, provider) {
1576
1713
  }
1577
1714
  function resolveCodexRoute(provider, model, apiKey) {
1578
1715
  const upstreamModelId2 = model.upstreamModelId || model.id;
1716
+ const inferredNpm = model.modelFormat === "anthropic" ? "@ai-sdk/anthropic" : "@ai-sdk/openai-compatible";
1717
+ const isZenGo = provider.id === "zen" || provider.id === "go";
1579
1718
  const base = {
1580
- npm: model.npm ?? (model.modelFormat === "anthropic" ? "@ai-sdk/anthropic" : "@ai-sdk/openai-compatible"),
1719
+ npm: isZenGo ? inferredNpm : model.npm ?? inferredNpm,
1581
1720
  baseURL: resolveBaseURL(model, provider),
1582
1721
  upstreamModelId: upstreamModelId2,
1583
1722
  apiKey,
1584
1723
  contextWindow: model.contextWindow,
1585
1724
  modelId: model.id,
1586
- providerId: provider.id
1725
+ providerId: provider.id,
1726
+ supportedParameters: model.supportedParameters,
1727
+ reasoning: model.reasoning,
1728
+ interleavedReasoningField: model.interleavedReasoningField
1587
1729
  };
1588
1730
  if (provider.id === "openai" && model.modelFormat === "openai") {
1589
1731
  return { tier: "direct", ...base };
@@ -1607,7 +1749,10 @@ function buildCodexProxyRoutesForProvider(provider, apiKey, selectedModelId, age
1607
1749
  apiKey: route.apiKey,
1608
1750
  baseURL: route.baseURL,
1609
1751
  upstreamModelId: route.upstreamModelId,
1610
- providerId: route.providerId
1752
+ providerId: route.providerId,
1753
+ supportedParameters: route.supportedParameters,
1754
+ reasoning: route.reasoning,
1755
+ interleavedReasoningField: route.interleavedReasoningField
1611
1756
  };
1612
1757
  });
1613
1758
  }
@@ -1665,8 +1810,8 @@ function buildCodexAppRootConfig(spec) {
1665
1810
  // src/codex/catalog.ts
1666
1811
  var DEFAULT_CONTEXT = 128e3;
1667
1812
  var CODEX_NO_REASONING_EFFORT = "none";
1668
- function codexCatalogReasoningFields(npm, wireId) {
1669
- const reasoning = getReasoningCapabilities(npm, wireId);
1813
+ function codexCatalogReasoningFields(npm, wireId, metadata) {
1814
+ const reasoning = getReasoningCapabilities(npm, wireId, metadata);
1670
1815
  if (reasoning.levels.length > 0) {
1671
1816
  return {
1672
1817
  supported_reasoning_levels: buildCodexReasoningLevels(reasoning),
@@ -1677,9 +1822,7 @@ function codexCatalogReasoningFields(npm, wireId) {
1677
1822
  }
1678
1823
  return {
1679
1824
  supported_reasoning_levels: buildCodexReasoningLevels({
1680
- levels: [CODEX_NO_REASONING_EFFORT],
1681
- defaultLevel: CODEX_NO_REASONING_EFFORT,
1682
- supportsSummaries: false
1825
+ levels: [CODEX_NO_REASONING_EFFORT]
1683
1826
  }),
1684
1827
  default_reasoning_level: CODEX_NO_REASONING_EFFORT,
1685
1828
  supports_reasoning_summaries: false,
@@ -1709,7 +1852,12 @@ function catalogEntryFromModel(model, providerName, priority, appCatalog = false
1709
1852
  const context = model.contextWindow ?? DEFAULT_CONTEXT;
1710
1853
  const label = formatCodexModelLabel(model);
1711
1854
  const wireId = model.upstreamModelId ?? model.id;
1712
- const reasoningFields = codexCatalogReasoningFields(model.npm ?? "", wireId);
1855
+ const reasoningFields = codexCatalogReasoningFields(model.npm ?? "", wireId, {
1856
+ apiBaseUrl: model.apiBaseUrl,
1857
+ supportedParameters: model.supportedParameters,
1858
+ reasoning: model.reasoning,
1859
+ interleavedReasoningField: model.interleavedReasoningField
1860
+ });
1713
1861
  return {
1714
1862
  slug,
1715
1863
  display_name: label,
@@ -2984,7 +3132,10 @@ function normalizeProviders(raw, opts) {
2984
3132
  npm: model.api?.npm,
2985
3133
  apiBaseUrl: model.api?.url,
2986
3134
  cost: model.cost,
2987
- contextWindow: resolveContextWindow(model.id, model.limit?.context)
3135
+ contextWindow: resolveContextWindow(model.id, model.limit?.context),
3136
+ supportedParameters: model.supportedParameters ?? model.supported_parameters,
3137
+ reasoning: model.reasoning,
3138
+ interleavedReasoningField: model.interleaved?.field
2988
3139
  });
2989
3140
  }
2990
3141
  if (models.length === 0) continue;
@@ -3123,7 +3274,10 @@ function modelToCached(model) {
3123
3274
  cost: model.cost,
3124
3275
  modelFormat: model.modelFormat,
3125
3276
  npm: model.npm,
3126
- apiUrl: model.apiBaseUrl
3277
+ apiUrl: model.apiBaseUrl,
3278
+ supportedParameters: model.supportedParameters,
3279
+ reasoning: model.reasoning,
3280
+ interleavedReasoningField: model.interleavedReasoningField
3127
3281
  };
3128
3282
  }
3129
3283
  function localProviderToRegistry(provider, opts) {
@@ -3293,7 +3447,8 @@ function parseModelList(body, npm) {
3293
3447
  brand: deriveBrand(family),
3294
3448
  contextWindow: resolveContextWindow(id),
3295
3449
  modelFormat: format,
3296
- npm
3450
+ npm,
3451
+ supportedParameters: Array.isArray(row.supported_parameters) ? row.supported_parameters : void 0
3297
3452
  });
3298
3453
  }
3299
3454
  return models;
@@ -3818,6 +3973,24 @@ var PROVIDER_TEMPLATES = [
3818
3973
  supported: false,
3819
3974
  unsupportedReason: "Uses gcloud Application Default Credentials \u2014 not supported via API key import."
3820
3975
  },
3976
+ {
3977
+ id: "zen",
3978
+ name: "OpenCode Zen",
3979
+ authType: "api",
3980
+ npm: "@ai-sdk/openai-compatible",
3981
+ signupUrl: "https://opencode.ai/auth",
3982
+ modelSource: "zen-go-api",
3983
+ supported: true
3984
+ },
3985
+ {
3986
+ id: "go",
3987
+ name: "OpenCode Go",
3988
+ authType: "api",
3989
+ npm: "@ai-sdk/openai-compatible",
3990
+ signupUrl: "https://opencode.ai/auth",
3991
+ modelSource: "zen-go-api",
3992
+ supported: true
3993
+ },
3821
3994
  // OAuth-gated subscription providers — use relay-ai providers auth <id> to sign in
3822
3995
  {
3823
3996
  id: "github-copilot",
@@ -3847,14 +4020,6 @@ function filterTemplates(templates, query) {
3847
4020
  (t) => t.id.toLowerCase().includes(q) || t.name.toLowerCase().includes(q) || t.npm.toLowerCase().includes(q)
3848
4021
  );
3849
4022
  }
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
4023
 
3859
4024
  // src/registry/resolve-template.ts
3860
4025
  var TEMPLATE_ID_ALIASES = {
@@ -3955,8 +4120,20 @@ async function validateImportKey(lp, entry) {
3955
4120
  }
3956
4121
  return reject("invalid-key", "No API base URL \u2014 cannot verify key.");
3957
4122
  }
4123
+ let safeBaseUrl = baseUrl;
4124
+ const configuredUrl = entry.api.url?.trim();
4125
+ const templateDefault = catalogTemplate?.defaultBaseUrl?.trim();
4126
+ if (configuredUrl && configuredUrl !== templateDefault) {
4127
+ const urlCheck = await validateCustomEndpointUrl(baseUrl, {
4128
+ allowInsecureLocal: catalogTemplate?.apiKeyOptional === true
4129
+ });
4130
+ if (!urlCheck.ok || !urlCheck.normalizedUrl) {
4131
+ return reject("invalid-key", `${urlCheck.error ?? "Invalid API base URL."} ${urlCheck.hint ?? ""}`.trim());
4132
+ }
4133
+ safeBaseUrl = urlCheck.normalizedUrl;
4134
+ }
3958
4135
  if (npm === "@ai-sdk/anthropic") {
3959
- const result2 = await fetchAnthropicModels(baseUrl, key);
4136
+ const result2 = await fetchAnthropicModels(safeBaseUrl, key);
3960
4137
  if (result2.error) {
3961
4138
  return reject(
3962
4139
  placeholder ? "placeholder-key" : "invalid-key",
@@ -3965,8 +4142,8 @@ async function validateImportKey(lp, entry) {
3965
4142
  }
3966
4143
  return { canImport: true };
3967
4144
  }
3968
- const template = catalogTemplate ?? syntheticTemplate(entry, baseUrl);
3969
- const result = await fetchTemplateModels(template, key, baseUrl);
4145
+ const template = catalogTemplate ?? syntheticTemplate(entry, safeBaseUrl);
4146
+ const result = await fetchTemplateModels(template, key, safeBaseUrl);
3970
4147
  if (result.error) {
3971
4148
  return reject(
3972
4149
  placeholder ? "placeholder-key" : "invalid-key",
@@ -4076,6 +4253,11 @@ async function importFromOpencode(options = {}) {
4076
4253
  continue;
4077
4254
  }
4078
4255
  }
4256
+ const saved = isOAuth ? await saveOAuthKey(lp.id, oauth) : await saveProviderKey(lp);
4257
+ if (!saved) {
4258
+ skipped.push({ id: lp.id, name: lp.name, reason: "credential-save-failed" });
4259
+ continue;
4260
+ }
4079
4261
  if (existingIdx >= 0) {
4080
4262
  registry.providers[existingIdx] = { ...entry, addedAt: registry.providers[existingIdx].addedAt };
4081
4263
  } else {
@@ -4083,11 +4265,8 @@ async function importFromOpencode(options = {}) {
4083
4265
  }
4084
4266
  imported.push(entry);
4085
4267
  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
- }
4268
+ keysSaved += 1;
4269
+ if (isOAuth) oauthImported += 1;
4091
4270
  }
4092
4271
  const alreadyReportedIds = new Set(skipped.map((s) => s.id));
4093
4272
  const registryProviderIds = new Set(registry.providers.map((p19) => p19.id));
@@ -4588,10 +4767,13 @@ function upstreamModelId(model) {
4588
4767
  const id = model.upstreamModelId ?? model.id;
4589
4768
  return id.replace(/\[1m\]$/i, "");
4590
4769
  }
4770
+ function isOpenAIChatCompletionsModel(model) {
4771
+ return model.modelFormat === "openai" && (!!model.completionsUrl || model.sourceBackend === "zen" || model.sourceBackend === "go");
4772
+ }
4591
4773
  function formatOpenAIModels(models) {
4592
4774
  return {
4593
4775
  object: "list",
4594
- data: models.map((model) => ({
4776
+ data: models.filter(isOpenAIChatCompletionsModel).map((model) => ({
4595
4777
  id: model.id,
4596
4778
  object: "model",
4597
4779
  created: CREATED_AT_UNIX,
@@ -4623,20 +4805,24 @@ function extractBearerToken(value) {
4623
4805
  }
4624
4806
 
4625
4807
  // src/upstream-forward.ts
4626
- function anthropicUpstreamHeaders(apiKey, stream = false) {
4808
+ function anthropicUpstreamHeaders(apiKey, stream = false, inboundBeta) {
4627
4809
  const key = sanitizeCredential(apiKey) ?? apiKey.trim();
4628
- return {
4810
+ const headers = {
4629
4811
  "Content-Type": "application/json",
4630
4812
  "anthropic-version": "2023-06-01",
4631
4813
  Authorization: `Bearer ${key}`,
4632
4814
  "x-api-key": key,
4633
4815
  ...stream ? { Accept: "text/event-stream" } : {}
4634
4816
  };
4817
+ if (inboundBeta) {
4818
+ headers["anthropic-beta"] = inboundBeta;
4819
+ }
4820
+ return headers;
4635
4821
  }
4636
- async function postJsonUpstream(url, body, apiKey) {
4822
+ async function postJsonUpstream(url, body, apiKey, inboundBeta) {
4637
4823
  const response = await fetch(url, {
4638
4824
  method: "POST",
4639
- headers: anthropicUpstreamHeaders(apiKey, false),
4825
+ headers: anthropicUpstreamHeaders(apiKey, false, inboundBeta),
4640
4826
  body: JSON.stringify(body)
4641
4827
  });
4642
4828
  const text5 = await response.text();
@@ -4656,12 +4842,12 @@ var UpstreamUnreachableError = class extends Error {
4656
4842
  this.name = "UpstreamUnreachableError";
4657
4843
  }
4658
4844
  };
4659
- async function relayAnthropicMessages(res, messagesUrl, body, apiKey, clientWantsStream) {
4845
+ async function relayAnthropicMessages(res, messagesUrl, body, apiKey, clientWantsStream, inboundBeta) {
4660
4846
  let upstreamRes;
4661
4847
  try {
4662
4848
  upstreamRes = await fetch(messagesUrl, {
4663
4849
  method: "POST",
4664
- headers: anthropicUpstreamHeaders(apiKey, clientWantsStream),
4850
+ headers: anthropicUpstreamHeaders(apiKey, clientWantsStream, inboundBeta),
4665
4851
  body: JSON.stringify(body)
4666
4852
  });
4667
4853
  } catch (err) {
@@ -4687,20 +4873,19 @@ async function relayAnthropicMessages(res, messagesUrl, body, apiKey, clientWant
4687
4873
  res.end(JSON.stringify({ type: "error", error: { type: "api_error", message: "Upstream returned empty response body" } }));
4688
4874
  return;
4689
4875
  }
4690
- let json;
4876
+ const text5 = await upstreamRes.text();
4691
4877
  try {
4692
- json = await upstreamRes.json();
4878
+ JSON.parse(text5);
4693
4879
  } catch {
4694
4880
  res.writeHead(502, { "Content-Type": "application/json" });
4695
4881
  res.end(JSON.stringify({ type: "error", error: { type: "api_error", message: "Upstream response was not valid JSON" } }));
4696
4882
  return;
4697
4883
  }
4698
- const payload = JSON.stringify(json);
4699
4884
  res.writeHead(200, {
4700
4885
  "Content-Type": "application/json",
4701
- "Content-Length": Buffer.byteLength(payload).toString()
4886
+ "Content-Length": Buffer.byteLength(text5).toString()
4702
4887
  });
4703
- res.end(payload);
4888
+ res.end(text5);
4704
4889
  }
4705
4890
 
4706
4891
  // src/sdk-adapter.ts
@@ -4717,7 +4902,7 @@ function silenceSdkWarnings() {
4717
4902
  sdkWarningsSilenced = true;
4718
4903
  globalThis.AI_SDK_LOG_WARNINGS = false;
4719
4904
  }
4720
- var TOOL_USE_SIG_SEP = "::ts::";
4905
+ var TOOL_USE_SIG_SEP = "__ts__";
4721
4906
  function parseToolArguments(value) {
4722
4907
  if (value === null || value === void 0) return {};
4723
4908
  if (typeof value === "object" && !Array.isArray(value)) return value;
@@ -4740,15 +4925,26 @@ data: ${JSON.stringify(data)}
4740
4925
  `;
4741
4926
  }
4742
4927
  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
- };
4928
+ let sep = id.lastIndexOf(TOOL_USE_SIG_SEP);
4929
+ if (sep !== -1) {
4930
+ return {
4931
+ rawId: id.slice(0, sep),
4932
+ thoughtSignature: Buffer.from(id.slice(sep + TOOL_USE_SIG_SEP.length), "base64url").toString("utf8")
4933
+ };
4934
+ }
4935
+ sep = id.lastIndexOf("::ts::");
4936
+ if (sep !== -1) {
4937
+ return {
4938
+ rawId: id.slice(0, sep),
4939
+ thoughtSignature: id.slice(sep + 6)
4940
+ };
4941
+ }
4942
+ return { rawId: id };
4749
4943
  }
4750
4944
  function encodeToolUseId(rawId, thoughtSignature) {
4751
- return thoughtSignature ? `${rawId}${TOOL_USE_SIG_SEP}${thoughtSignature}` : rawId;
4945
+ if (!thoughtSignature) return rawId;
4946
+ const encoded = Buffer.from(thoughtSignature, "utf8").toString("base64url");
4947
+ return `${rawId}${TOOL_USE_SIG_SEP}${encoded}`;
4752
4948
  }
4753
4949
  function serializeToolResultContent(content) {
4754
4950
  return typeof content === "string" ? content : JSON.stringify(content);
@@ -4950,7 +5146,7 @@ function translateRequest(body, npm, options) {
4950
5146
  const effort = anthropicEffortFromRequest(body) ?? options?.defaultEffort;
4951
5147
  const providerOptions = deepMergeProviderOptions(
4952
5148
  thinkingProviderOptions(npm),
4953
- effortProviderOptions(npm, effort, body.model)
5149
+ effortProviderOptions(npm, effort, body.model, options?.reasoningMetadata)
4954
5150
  );
4955
5151
  return {
4956
5152
  system,
@@ -5133,7 +5329,7 @@ async function generateAnthropicResponse(model, params, modelId) {
5133
5329
  ...r.text ? [{ type: "text", text: r.text }] : [],
5134
5330
  ...r.toolCalls.map((tc) => ({
5135
5331
  type: "tool_use",
5136
- id: tc.toolCallId,
5332
+ id: encodeToolUseId(tc.toolCallId, grabRoundTripSignature(tc)),
5137
5333
  name: tc.toolName,
5138
5334
  input: tc.input
5139
5335
  }))
@@ -5259,11 +5455,13 @@ function startProxyCatalog(routes, defaultAliasId, debug = false) {
5259
5455
  return;
5260
5456
  }
5261
5457
  if (route.modelFormat === "anthropic") {
5458
+ const betaHeaderRaw = req.headers["anthropic-beta"];
5459
+ const inboundBeta = Array.isArray(betaHeaderRaw) ? betaHeaderRaw.join(",") : betaHeaderRaw;
5262
5460
  const forwardBody = { ...anthropicBody, model: route.realModelId };
5263
5461
  const targetUrl = `${upstreamUrl}/v1/messages`;
5264
5462
  plog(() => `anthropic-passthrough: model=${route.realModelId}, stream=${clientWantsStream}`);
5265
5463
  try {
5266
- await relayAnthropicMessages(res, targetUrl, forwardBody, apiKey, clientWantsStream);
5464
+ await relayAnthropicMessages(res, targetUrl, forwardBody, apiKey, clientWantsStream, inboundBeta);
5267
5465
  } catch (err) {
5268
5466
  const message = err instanceof UpstreamUnreachableError ? err.message : String(err);
5269
5467
  plog(() => `anthropic-passthrough error: ${message}`);
@@ -5272,7 +5470,15 @@ function startProxyCatalog(routes, defaultAliasId, debug = false) {
5272
5470
  return;
5273
5471
  }
5274
5472
  if (isSdkMigratedNpm(route.npm)) {
5275
- const params = translateRequest(anthropicBody, route.npm);
5473
+ const params = translateRequest(anthropicBody, route.npm, {
5474
+ reasoningMetadata: {
5475
+ providerId: route.providerId,
5476
+ apiBaseUrl: route.baseURL,
5477
+ supportedParameters: route.supportedParameters,
5478
+ reasoning: route.reasoning,
5479
+ interleavedReasoningField: route.interleavedReasoningField
5480
+ }
5481
+ });
5276
5482
  plog(
5277
5483
  () => `sdk: npm=${route.npm} model=${route.realModelId}, stream=${clientWantsStream}, tools=${anthropicBody.tools?.length ?? 0}, msgs=${params.messages.length}`
5278
5484
  );
@@ -5349,7 +5555,11 @@ function startProxy(completionsUrl, modelId, debug = false, contextWindow, sdk)
5349
5555
  modelFormat: "openai",
5350
5556
  contextWindow,
5351
5557
  npm: sdk?.npm,
5352
- baseURL: sdk?.baseURL
5558
+ baseURL: sdk?.baseURL,
5559
+ providerId: sdk?.providerId,
5560
+ supportedParameters: sdk?.supportedParameters,
5561
+ reasoning: sdk?.reasoning,
5562
+ interleavedReasoningField: sdk?.interleavedReasoningField
5353
5563
  }], clientModelId, debug);
5354
5564
  }
5355
5565
 
@@ -5366,7 +5576,11 @@ function localModelToRoute(lp, model) {
5366
5576
  modelFormat: model.modelFormat,
5367
5577
  contextWindow: model.contextWindow,
5368
5578
  npm: model.npm,
5369
- baseURL: model.apiBaseUrl
5579
+ baseURL: model.apiBaseUrl,
5580
+ providerId: lp.id,
5581
+ supportedParameters: model.supportedParameters,
5582
+ reasoning: model.reasoning,
5583
+ interleavedReasoningField: model.interleavedReasoningField
5370
5584
  };
5371
5585
  }
5372
5586
  function zenGoModelToRoute(model, apiKey) {
@@ -5384,7 +5598,8 @@ function zenGoModelToRoute(model, apiKey) {
5384
5598
  // openai-format Zen/Go models route through the SDK (openai-compatible);
5385
5599
  // anthropic models stay direct passthrough (no npm).
5386
5600
  npm: isAnthropic ? void 0 : "@ai-sdk/openai-compatible",
5387
- baseURL: isAnthropic ? void 0 : `${backend.baseUrl}/v1`
5601
+ baseURL: isAnthropic ? void 0 : `${backend.baseUrl}/v1`,
5602
+ providerId: model.sourceBackend
5388
5603
  };
5389
5604
  }
5390
5605
  function makeRouteResolver(localProviders, zenModels, goModels, zenGoApiKey) {
@@ -5554,6 +5769,7 @@ function cachedModelToLocal(cached, provider) {
5554
5769
  const apiUrl = cached.apiUrl ?? provider.api.url ?? "";
5555
5770
  const endpoint = resolveEndpoint(npm, apiUrl);
5556
5771
  if (endpoint === null) return null;
5772
+ const modelsDev = findModelsDevModel(provider.id, cached.id);
5557
5773
  const { id, upstreamModelId: upstreamModelId2 } = normalizeGoogleModelId(cached.id, npm);
5558
5774
  const normalizedUpstream = normalizeGoogleModelId(cached.upstreamModelId ?? cached.id, npm).upstreamModelId;
5559
5775
  const family = npm === "@ai-sdk/google" ? id.split(/[-/:]/)[0] ?? id : cached.family ?? "";
@@ -5569,7 +5785,10 @@ function cachedModelToLocal(cached, provider) {
5569
5785
  npm: npm || void 0,
5570
5786
  apiBaseUrl: apiUrl || void 0,
5571
5787
  cost: cached.cost,
5572
- contextWindow: cached.contextWindow ?? resolveContextWindow(id)
5788
+ contextWindow: cached.contextWindow ?? resolveContextWindow(id),
5789
+ supportedParameters: cached.supportedParameters,
5790
+ reasoning: cached.reasoning ?? modelsDev?.reasoning,
5791
+ interleavedReasoningField: cached.interleavedReasoningField ?? modelsDev?.interleaved?.field
5573
5792
  };
5574
5793
  }
5575
5794
  function materializeOne(provider, resolveCredential, agent) {
@@ -5770,7 +5989,10 @@ function localProvidersToServerModels(localProviders) {
5770
5989
  npm: model.modelFormat === "openai" ? model.npm || "@ai-sdk/openai-compatible" : model.npm,
5771
5990
  apiBaseUrl: model.apiBaseUrl,
5772
5991
  apiKey: provider.apiKey,
5773
- contextWindow: model.contextWindow
5992
+ contextWindow: model.contextWindow,
5993
+ supportedParameters: model.supportedParameters,
5994
+ reasoning: model.reasoning,
5995
+ interleavedReasoningField: model.interleavedReasoningField
5774
5996
  }))
5775
5997
  );
5776
5998
  }
@@ -5893,11 +6115,18 @@ async function askSaveServerPassword() {
5893
6115
 
5894
6116
  // src/server/router.ts
5895
6117
  import { createServer as createServer2 } from "http";
6118
+ function makeServerLog(debugLogPath) {
6119
+ if (!debugLogPath) return () => {
6120
+ };
6121
+ resetTraceLog(debugLogPath);
6122
+ return (msg) => writeSecureLogLine(debugLogPath, typeof msg === "function" ? msg() : msg);
6123
+ }
5896
6124
  async function startServer(options) {
5897
6125
  silenceSdkWarnings();
5898
6126
  const languageModelCache = /* @__PURE__ */ new Map();
6127
+ const plog = makeServerLog(options.debugLogPath);
5899
6128
  const server = createServer2((req, res) => {
5900
- void routeRequest(req, res, options, languageModelCache);
6129
+ void routeRequest(req, res, options, languageModelCache, plog);
5901
6130
  });
5902
6131
  await new Promise((resolve, reject2) => {
5903
6132
  server.once("error", reject2);
@@ -5920,9 +6149,10 @@ async function startServer(options) {
5920
6149
  })
5921
6150
  };
5922
6151
  }
5923
- async function routeRequest(req, res, options, modelCache) {
6152
+ async function routeRequest(req, res, options, modelCache, plog) {
5924
6153
  try {
5925
6154
  const pathname = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`).pathname;
6155
+ plog(`${req.method} ${pathname}`);
5926
6156
  if (req.method === "GET" && pathname === "/health") {
5927
6157
  sendJson(res, 200, { ok: true });
5928
6158
  return;
@@ -5944,7 +6174,7 @@ async function routeRequest(req, res, options, modelCache) {
5944
6174
  return;
5945
6175
  }
5946
6176
  if (req.method === "POST" && pathname === "/anthropic/v1/messages") {
5947
- await handleAnthropicMessages(req, res, options, modelCache);
6177
+ await handleAnthropicMessages(req, res, options, modelCache, plog);
5948
6178
  return;
5949
6179
  }
5950
6180
  if (req.method === "POST" && pathname === "/openai/v1/chat/completions") {
@@ -5956,14 +6186,18 @@ async function routeRequest(req, res, options, modelCache) {
5956
6186
  sendJson(res, 500, { error: { message: err instanceof Error ? err.message : String(err) } });
5957
6187
  }
5958
6188
  }
5959
- async function handleAnthropicMessages(req, res, options, modelCache) {
6189
+ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
5960
6190
  const body = await readJson(req);
5961
6191
  if (!body) {
5962
6192
  sendJson(res, 400, { error: { message: "Invalid JSON body" } });
5963
6193
  return;
5964
6194
  }
5965
6195
  const model = lookupModel(res, options.catalog, body.model);
5966
- if (!model) return;
6196
+ if (!model) {
6197
+ plog(`model not found: ${body.model}`);
6198
+ return;
6199
+ }
6200
+ plog(() => `anthropic-messages model=${body.model} format=${model.modelFormat} npm=${model.npm ?? "none"} stream=${body.stream}`);
5967
6201
  if (model.modelFormat === "anthropic") {
5968
6202
  if (model.baseUrl && !/^https?:\/\//i.test(model.baseUrl)) {
5969
6203
  sendJson(res, 400, { error: { message: `Invalid provider baseUrl: must be http:// or https://` } });
@@ -5971,7 +6205,10 @@ async function handleAnthropicMessages(req, res, options, modelCache) {
5971
6205
  }
5972
6206
  const messagesUrl = model.baseUrl ? `${model.baseUrl}/v1/messages` : `${backendFor(options, model).baseUrl}/v1/messages`;
5973
6207
  const apiKey = model.apiKey ?? options.apiKey;
5974
- await forwardJson(res, messagesUrl, { ...body, model: upstreamModelId(model) }, apiKey);
6208
+ const betaHeaderRaw = req.headers["anthropic-beta"];
6209
+ const inboundBeta = Array.isArray(betaHeaderRaw) ? betaHeaderRaw.join(",") : betaHeaderRaw;
6210
+ plog(() => `anthropic-passthrough \u2192 ${messagesUrl}`);
6211
+ await forwardJson(res, messagesUrl, { ...body, model: upstreamModelId(model) }, apiKey, inboundBeta);
5975
6212
  return;
5976
6213
  }
5977
6214
  if (model.modelFormat === "openai") {
@@ -5980,22 +6217,32 @@ async function handleAnthropicMessages(req, res, options, modelCache) {
5980
6217
  return;
5981
6218
  }
5982
6219
  const apiKey = model.apiKey ?? options.apiKey;
5983
- let languageModel = modelCache.get(model.id);
6220
+ const cacheKey = sdkModelCacheKey(model);
6221
+ let languageModel = modelCache.get(cacheKey);
5984
6222
  if (!languageModel) {
5985
6223
  languageModel = await createLanguageModel({
5986
6224
  npm: model.npm,
5987
6225
  modelId: upstreamModelId(model),
5988
6226
  apiKey,
5989
6227
  baseURL: model.apiBaseUrl,
5990
- providerId: model.sourceBackend,
6228
+ providerId: model.providerId ?? model.sourceBackend,
5991
6229
  vertex: options.vertex
5992
6230
  });
5993
- modelCache.set(model.id, languageModel);
6231
+ modelCache.set(cacheKey, languageModel);
5994
6232
  }
5995
6233
  const params = translateRequest(body, model.npm, {
5996
- defaultEffort: anthropicEffortFromRequest(body) ? void 0 : model.defaultEffort
6234
+ defaultEffort: anthropicEffortFromRequest(body) ? void 0 : model.defaultEffort,
6235
+ reasoningMetadata: {
6236
+ providerId: model.providerId,
6237
+ apiBaseUrl: model.apiBaseUrl,
6238
+ supportedParameters: model.supportedParameters,
6239
+ reasoning: model.reasoning,
6240
+ interleavedReasoningField: model.interleavedReasoningField
6241
+ }
5997
6242
  });
5998
6243
  const clientWantsStream = Boolean(body.stream);
6244
+ const responseModelId = options.gateway?.maskGatewayIds ? gatewayDisplayName(model, options.gateway) : typeof body.model === "string" ? body.model : model.id;
6245
+ plog(() => `sdk npm=${model.npm} upstream=${upstreamModelId(model)} responseModel=${responseModelId} stream=${clientWantsStream}`);
5999
6246
  try {
6000
6247
  if (clientWantsStream) {
6001
6248
  res.writeHead(200, {
@@ -6003,12 +6250,10 @@ async function handleAnthropicMessages(req, res, options, modelCache) {
6003
6250
  "Cache-Control": "no-cache",
6004
6251
  "Connection": "keep-alive"
6005
6252
  });
6006
- const clientModel = typeof body.model === "string" ? body.model : model.id;
6007
- await streamAnthropicResponse(languageModel, params, clientModel, (chunk) => res.write(chunk));
6253
+ await streamAnthropicResponse(languageModel, params, responseModelId, (chunk) => res.write(chunk));
6008
6254
  res.end();
6009
6255
  } else {
6010
- const clientModel = typeof body.model === "string" ? body.model : model.id;
6011
- const anthropicResponse = await generateAnthropicResponse(languageModel, params, clientModel);
6256
+ const anthropicResponse = await generateAnthropicResponse(languageModel, params, responseModelId);
6012
6257
  sendJson(res, 200, anthropicResponse);
6013
6258
  }
6014
6259
  } catch (err) {
@@ -6029,6 +6274,14 @@ async function handleOpenAIChatCompletions(req, res, options) {
6029
6274
  const model = lookupModel(res, options.catalog, body.model);
6030
6275
  if (!model) return;
6031
6276
  if (model.modelFormat === "openai") {
6277
+ if (!isOpenAIChatCompletionsModel(model)) {
6278
+ sendJson(res, 400, {
6279
+ error: {
6280
+ message: `OpenAI chat completions are not available for model: ${model.id}. Use /anthropic/v1/messages.`
6281
+ }
6282
+ });
6283
+ return;
6284
+ }
6032
6285
  if (model.completionsUrl && !/^https?:\/\//i.test(model.completionsUrl)) {
6033
6286
  sendJson(res, 400, { error: { message: `Invalid provider completionsUrl: must be http:// or https://` } });
6034
6287
  return;
@@ -6064,15 +6317,22 @@ function backendFor(options, model) {
6064
6317
  if (model.sourceBackend === "go") return options.backends.go;
6065
6318
  throw new Error(`Provider ${model.sourceBackend} is not a cloud backend \u2014 model must set baseUrl/completionsUrl`);
6066
6319
  }
6067
- async function forwardJson(res, url, body, apiKey) {
6068
- const upstream = await postJsonUpstream(url, body, apiKey);
6320
+ function sdkModelCacheKey(model) {
6321
+ return [
6322
+ model.providerId ?? model.sourceBackend,
6323
+ model.id,
6324
+ upstreamModelId(model),
6325
+ model.npm ?? "",
6326
+ model.apiBaseUrl ?? ""
6327
+ ].join("");
6328
+ }
6329
+ async function forwardJson(res, url, body, apiKey, inboundBeta) {
6330
+ const upstream = await postJsonUpstream(url, body, apiKey, inboundBeta);
6069
6331
  sendJson(res, upstream.status, upstream.body);
6070
6332
  }
6071
6333
  async function readJson(req) {
6072
6334
  try {
6073
- const chunks = [];
6074
- for await (const chunk of req) chunks.push(Buffer.from(chunk));
6075
- const raw = Buffer.concat(chunks).toString();
6335
+ const raw = await readBody(req);
6076
6336
  return raw ? JSON.parse(raw) : {};
6077
6337
  } catch {
6078
6338
  return null;
@@ -6228,7 +6488,9 @@ function resolveVertexLocation(env = process.env) {
6228
6488
  function defaultAdcCredentialsPath(home = homedir7()) {
6229
6489
  return join10(home, ".config", "gcloud", "application_default_credentials.json");
6230
6490
  }
6231
- function hasApplicationDefaultCredentials(home = homedir7(), adcPath = defaultAdcCredentialsPath(home)) {
6491
+ function hasApplicationDefaultCredentials(home = homedir7(), adcPath = defaultAdcCredentialsPath(home), env = process.env) {
6492
+ const explicitPath = env["GOOGLE_APPLICATION_CREDENTIALS"]?.trim();
6493
+ if (explicitPath && existsSync10(explicitPath)) return true;
6232
6494
  return existsSync10(adcPath);
6233
6495
  }
6234
6496
  function loadVertexModelEntries(env = process.env) {
@@ -6259,19 +6521,23 @@ function buildVertexRuntimeConfig(env = process.env) {
6259
6521
  };
6260
6522
  }
6261
6523
  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
- }));
6524
+ return config.models.map((model) => {
6525
+ const caps = getReasoningCapabilities(VERTEX_ANTHROPIC_NPM, model.upstream_id ?? model.id);
6526
+ return {
6527
+ id: model.id,
6528
+ name: model.display_name,
6529
+ isFree: false,
6530
+ brand: "Anthropic",
6531
+ sourceBackend: "vertex",
6532
+ modelFormat: "openai",
6533
+ upstreamModelId: model.upstream_id ?? model.id,
6534
+ npm: VERTEX_ANTHROPIC_NPM,
6535
+ providerLabel: "Vertex AI",
6536
+ providerId: "vertex",
6537
+ contextWindow: resolveContextWindow(model.id),
6538
+ ...caps.defaultLevel ? { defaultEffort: caps.defaultLevel } : {}
6539
+ };
6540
+ });
6275
6541
  }
6276
6542
  function vertexClientModelLookupCandidates(modelId) {
6277
6543
  const candidates = [modelId];
@@ -6388,7 +6654,13 @@ async function loadServerModels() {
6388
6654
  }
6389
6655
  function enrichServerModelReasoning(model) {
6390
6656
  if (!model.npm || model.modelFormat !== "openai") return model;
6391
- const caps = getReasoningCapabilities(model.npm, upstreamModelId(model));
6657
+ const caps = getReasoningCapabilities(model.npm, upstreamModelId(model), {
6658
+ providerId: model.providerId,
6659
+ apiBaseUrl: model.apiBaseUrl,
6660
+ supportedParameters: model.supportedParameters,
6661
+ reasoning: model.reasoning,
6662
+ interleavedReasoningField: model.interleavedReasoningField
6663
+ });
6392
6664
  if (!caps.defaultLevel) return model;
6393
6665
  return { ...model, defaultEffort: caps.defaultLevel };
6394
6666
  }
@@ -6974,6 +7246,71 @@ async function pickGlobalFavoriteModel(providers, favorites) {
6974
7246
  }
6975
7247
  }
6976
7248
 
7249
+ // src/favorites-resolver.ts
7250
+ var ZEN_GO_PROVIDER_NAME = {
7251
+ zen: "OpenCode Zen",
7252
+ go: "OpenCode Go"
7253
+ };
7254
+ function resolveFavorite(fav, ctx) {
7255
+ if (fav.providerId === "zen" || fav.providerId === "go") {
7256
+ if (!ctx.zenGoApiKey) return void 0;
7257
+ const models = fav.providerId === "zen" ? ctx.zenModels : ctx.goModels;
7258
+ const model = models?.find((m) => m.id === fav.modelId);
7259
+ if (!model) return void 0;
7260
+ return {
7261
+ providerId: fav.providerId,
7262
+ providerName: ZEN_GO_PROVIDER_NAME[fav.providerId],
7263
+ model,
7264
+ apiKey: ctx.zenGoApiKey,
7265
+ sourceBackend: fav.providerId
7266
+ };
7267
+ }
7268
+ if (ctx.findLocalModel) {
7269
+ const found = ctx.findLocalModel(fav.providerId, fav.modelId);
7270
+ if (!found) return void 0;
7271
+ if (ctx.agent && shouldHideModel({ providerId: fav.providerId, modelId: fav.modelId, agent: ctx.agent })) {
7272
+ return void 0;
7273
+ }
7274
+ return {
7275
+ providerId: fav.providerId,
7276
+ providerName: found.provider.name,
7277
+ model: found.model,
7278
+ apiKey: found.provider.apiKey
7279
+ };
7280
+ }
7281
+ return void 0;
7282
+ }
7283
+ function buildFavoritesList(starting, favorites, ctx, max = 20) {
7284
+ const droppedFavorites = [];
7285
+ const seen = /* @__PURE__ */ new Set();
7286
+ const out = [];
7287
+ if (starting) {
7288
+ seen.add(`${starting.providerId}::${starting.model.id}`);
7289
+ out.push(starting);
7290
+ }
7291
+ for (const fav of favorites) {
7292
+ if (out.length >= max) break;
7293
+ const key = `${fav.providerId}::${fav.modelId}`;
7294
+ if (seen.has(key)) continue;
7295
+ const resolved = resolveFavorite(fav, ctx);
7296
+ if (!resolved) {
7297
+ droppedFavorites.push(fav);
7298
+ continue;
7299
+ }
7300
+ seen.add(key);
7301
+ out.push(resolved);
7302
+ }
7303
+ return { resolved: out, droppedFavorites };
7304
+ }
7305
+ function resolveFirstAvailableFavorite(favorites, providers) {
7306
+ for (const fav of favorites) {
7307
+ const provider = providers.find((lp) => lp.id === fav.providerId);
7308
+ const model = provider?.models.find((m) => m.id === fav.modelId);
7309
+ if (provider && model) return { provider, model };
7310
+ }
7311
+ return void 0;
7312
+ }
7313
+
6977
7314
  // src/providers-command.ts
6978
7315
  import pc10 from "picocolors";
6979
7316
  import * as p10 from "@clack/prompts";
@@ -7136,18 +7473,35 @@ function modelInfoToCached(m, npm, apiUrl) {
7136
7473
  async function refreshZenGoProvider(provider) {
7137
7474
  const backendId = provider.id === "go" || provider.templateId === "go" ? "go" : "zen";
7138
7475
  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`));
7476
+ return result.models.filter((m) => m.modelFormat !== "unsupported").map((m) => {
7477
+ const isAnthropic = m.modelFormat === "anthropic";
7478
+ const npm = isAnthropic ? "@ai-sdk/anthropic" : "@ai-sdk/openai-compatible";
7479
+ const apiUrl = isAnthropic ? BACKENDS[backendId].baseUrl : `${BACKENDS[backendId].baseUrl}/v1`;
7480
+ return modelInfoToCached(m, npm, apiUrl);
7481
+ });
7140
7482
  }
7141
7483
  async function refreshApiListProvider(provider, apiKey) {
7142
7484
  const npm = provider.api.npm ?? "@ai-sdk/openai-compatible";
7143
7485
  const catalogTemplate = resolveProviderTemplate(provider);
7144
7486
  const baseUrl = effectiveProviderBaseUrl(provider, catalogTemplate);
7145
- const template = catalogTemplate ?? syntheticTemplate(provider, baseUrl);
7146
7487
  if (!baseUrl) {
7147
7488
  return { models: [], error: "Provider has no API base URL configured." };
7148
7489
  }
7490
+ let safeBaseUrl = baseUrl;
7491
+ const configuredUrl = provider.api.url?.trim();
7492
+ const templateDefault = catalogTemplate?.defaultBaseUrl?.trim();
7493
+ if (configuredUrl && configuredUrl !== templateDefault) {
7494
+ const urlCheck = await validateCustomEndpointUrl(baseUrl, {
7495
+ allowInsecureLocal: catalogTemplate?.apiKeyOptional === true
7496
+ });
7497
+ if (!urlCheck.ok || !urlCheck.normalizedUrl) {
7498
+ return { models: [], error: `${urlCheck.error ?? "Invalid API base URL."} ${urlCheck.hint ?? ""}`.trim() };
7499
+ }
7500
+ safeBaseUrl = urlCheck.normalizedUrl;
7501
+ }
7502
+ const template = catalogTemplate ?? syntheticTemplate(provider, safeBaseUrl);
7149
7503
  if (npm === "@ai-sdk/anthropic") {
7150
- const fetched2 = await fetchAnthropicModels(baseUrl, apiKey);
7504
+ const fetched2 = await fetchAnthropicModels(safeBaseUrl, apiKey);
7151
7505
  if (fetched2.error || fetched2.models.length === 0) {
7152
7506
  return { models: [], error: fetched2.error ?? "No models returned.", baseUrl: fetched2.baseUrl };
7153
7507
  }
@@ -7156,7 +7510,7 @@ async function refreshApiListProvider(provider, apiKey) {
7156
7510
  baseUrl: fetched2.baseUrl
7157
7511
  };
7158
7512
  }
7159
- const fetched = await fetchTemplateModels(template, apiKey, baseUrl);
7513
+ const fetched = await fetchTemplateModels(template, apiKey, safeBaseUrl);
7160
7514
  if (fetched.error || fetched.models.length === 0) {
7161
7515
  return { models: [], error: fetched.error ?? "No models returned." };
7162
7516
  }
@@ -7206,7 +7560,10 @@ async function refreshProviderModels(providerId, apiKey, registry = loadRegistry
7206
7560
  if (source === "zen-go-api") {
7207
7561
  models = await refreshZenGoProvider(provider);
7208
7562
  } else {
7209
- if (isLikelyPlaceholderKey(apiKey)) {
7563
+ const template = resolveProviderTemplate(provider);
7564
+ const keyOptional = template?.apiKeyOptional === true;
7565
+ const effectiveKey = keyOptional && isLikelyPlaceholderKey(apiKey) ? "" : apiKey;
7566
+ if (!keyOptional && isLikelyPlaceholderKey(effectiveKey)) {
7210
7567
  if (cachedModelCount(provider) > 0) {
7211
7568
  return skipWithCachedModels(
7212
7569
  provider,
@@ -7220,7 +7577,7 @@ async function refreshProviderModels(providerId, apiKey, registry = loadRegistry
7220
7577
  reason: "No usable API key \u2014 add the provider via relay-ai providers add with a real key."
7221
7578
  };
7222
7579
  }
7223
- if (!apiKey) {
7580
+ if (!keyOptional && !effectiveKey) {
7224
7581
  return {
7225
7582
  id: provider.id,
7226
7583
  name: provider.name,
@@ -7228,7 +7585,7 @@ async function refreshProviderModels(providerId, apiKey, registry = loadRegistry
7228
7585
  reason: "API key not available \u2014 cannot refresh models."
7229
7586
  };
7230
7587
  }
7231
- const fetched = await refreshApiListProvider(provider, apiKey);
7588
+ const fetched = await refreshApiListProvider(provider, effectiveKey ?? "");
7232
7589
  if (fetched.error) {
7233
7590
  if ((fetched.error.includes("rejected") || fetched.error.includes("401") || fetched.error.includes("403")) && cachedModelCount(provider) > 0) {
7234
7591
  return skipWithCachedModels(
@@ -7425,7 +7782,10 @@ async function authenticateProvider(providerId, options = {}) {
7425
7782
  if (!supportsNativeOAuth(providerId)) {
7426
7783
  if (findOpencodeBinary()) {
7427
7784
  const cred2 = await runOpencodeAuthBroker(providerId, { method: options.brokerMethod });
7428
- await saveProviderCredential(oauthAuthRef(providerId), oauthCredentialToKeychainJson(cred2));
7785
+ const saved2 = await saveProviderCredential(oauthAuthRef(providerId), oauthCredentialToKeychainJson(cred2));
7786
+ if (!saved2) {
7787
+ p9.log.warn("Could not save OAuth tokens to Keychain \u2014 session may not persist.");
7788
+ }
7429
7789
  const registryProvider2 = await upsertOAuthProvider(providerId, cred2);
7430
7790
  return { providerId, credential: cred2, registryProvider: registryProvider2 };
7431
7791
  }
@@ -7547,7 +7907,7 @@ ${pc10.bold("Usage:")}
7547
7907
  ${pc10.bold("Subcommands:")}
7548
7908
  (none) Provider hub wizard ${pc10.dim("[Phase 1.1]")}
7549
7909
  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]")}
7910
+ import import providers from 'open code CLI' (one-time) ${pc10.dim("[Phase 1.0]")}
7551
7911
  auth Sign in with OAuth (xAI, OpenAI ChatGPT) ${pc10.dim("[Phase 2]")}
7552
7912
  list Show configured providers ${pc10.dim("[Phase 1.0]")}
7553
7913
  remove Remove a provider by id ${pc10.dim("[Phase 1.1]")}
@@ -7596,7 +7956,7 @@ async function runProvidersImport() {
7596
7956
  );
7597
7957
  if (result.skipped.length > 0) {
7598
7958
  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;
7959
+ 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
7960
  p10.log.warn(`Skipped ${s.name} (${s.id}): ${reason}`);
7601
7961
  }
7602
7962
  }
@@ -7607,6 +7967,18 @@ async function runProvidersImport() {
7607
7967
  }
7608
7968
  }
7609
7969
  }
7970
+ if (result.imported.length > 0) {
7971
+ const refreshSpinner = p10.spinner();
7972
+ refreshSpinner.start("Fetching model capabilities from providers...");
7973
+ await Promise.all(result.imported.map(async (provider) => {
7974
+ const key = await resolveRefreshCredential(
7975
+ provider,
7976
+ async (pr) => resolveProviderCredential(pr.id, pr.authRef)
7977
+ );
7978
+ await refreshProviderModels(provider.id, key);
7979
+ }));
7980
+ refreshSpinner.stop("Model capabilities refreshed.");
7981
+ }
7610
7982
  return 0;
7611
7983
  }
7612
7984
  async function runProvidersAuth(providerId, method) {
@@ -7696,7 +8068,9 @@ async function runProvidersList() {
7696
8068
  }
7697
8069
  async function pickTemplateFromCatalog() {
7698
8070
  while (true) {
7699
- const templates = listAddableTemplates(loadRegistry().providers.map((p19) => p19.id));
8071
+ const registry = loadRegistry();
8072
+ const configuredIds = new Set(registry.providers.map((p19) => p19.id));
8073
+ const templates = listAddableTemplates(configuredIds);
7700
8074
  if (templates.length === 0) return null;
7701
8075
  const method = await p10.select({
7702
8076
  message: `Choose a provider (${templates.length} available)`,
@@ -7727,22 +8101,12 @@ async function pickTemplateFromCatalog() {
7727
8101
  const query = String(searchInput);
7728
8102
  const matched = filterTemplates(templates, query);
7729
8103
  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;
8104
+ const alreadyAdded = filterTemplates(listSupportedTemplates(), query).filter((t) => configuredIds.has(t.id));
8105
+ if (alreadyAdded.length > 0) {
8106
+ p10.log.info(`Already configured: ${alreadyAdded.map((t) => t.name).join(", ")}`);
8107
+ } else {
8108
+ p10.log.warn("No providers match \u2014 try a different search");
7744
8109
  }
7745
- p10.log.warn("No providers match \u2014 try a different search");
7746
8110
  continue;
7747
8111
  }
7748
8112
  const options = matched.map((t) => ({
@@ -7766,6 +8130,40 @@ async function runTemplateAddFlow() {
7766
8130
  }
7767
8131
  const template = await pickTemplateFromCatalog();
7768
8132
  if (!template) return 0;
8133
+ if (template.modelSource === "zen-go-api") {
8134
+ const existingKey = await readGlobalOpencodeCredential();
8135
+ let apiKey2 = existingKey;
8136
+ if (!apiKey2) {
8137
+ printPanel(pc10.cyan("OpenCode cloud"), [
8138
+ `${pc10.white("Get an API key at:")} ${fmtUrl("https://opencode.ai/auth")}`,
8139
+ `${pc10.dim("Uses OpenCode Zen / Go cloud models \u2014 not the same as importing from the OpenCode CLI.")}`
8140
+ ]);
8141
+ const collected = await resolveOrCollectApiKey(false, false);
8142
+ if (!collected) {
8143
+ p10.cancel("Cancelled.");
8144
+ return 0;
8145
+ }
8146
+ apiKey2 = collected;
8147
+ }
8148
+ await migrateGlobalOpencodeCredential();
8149
+ const spinner10 = p10.spinner();
8150
+ spinner10.start(`Adding ${template.name}...`);
8151
+ const stub = template.id === "zen" ? addZenRegistryStub() : addGoRegistryStub();
8152
+ if (!stub.added && stub.reason) {
8153
+ spinner10.stop("");
8154
+ p10.log.warn(stub.reason);
8155
+ return 0;
8156
+ }
8157
+ const registry = loadRegistry();
8158
+ const refreshResult = await refreshProviderModels(template.id, apiKey2, registry);
8159
+ spinner10.stop("");
8160
+ if (refreshResult.ok) {
8161
+ p10.log.success(`Added ${template.name} \u2014 ${fmtCount(refreshResult.modelCount ?? 0, "model")} updated.`);
8162
+ } else {
8163
+ p10.log.warn(`Added ${template.name}, but model refresh failed: ${refreshResult.reason ?? "Unknown error"}`);
8164
+ }
8165
+ return 0;
8166
+ }
7769
8167
  if (template.signupUrl) {
7770
8168
  printPanel(fmtProvider(template.name), [
7771
8169
  `${pc10.white("Get an API key at:")} ${fmtUrl(template.signupUrl)}`
@@ -7811,66 +8209,6 @@ async function runTemplateAddFlow() {
7811
8209
  logConnected(template.name, result.modelCount ?? 0);
7812
8210
  return 0;
7813
8211
  }
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
8212
  async function runCustomEndpointAddFlow() {
7875
8213
  const kindChoice = await p10.select({
7876
8214
  message: "Custom server type",
@@ -7932,14 +8270,9 @@ async function runProvidersAdd() {
7932
8270
  const registry = loadRegistry();
7933
8271
  const hasOpencode = findOpencodeBinary() !== null;
7934
8272
  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
8273
  {
7941
8274
  value: "import",
7942
- label: "Bring settings from OpenCode CLI",
8275
+ label: "import providers from 'open code CLI'",
7943
8276
  hint: hasOpencode ? "Import Groq, OpenAI, etc. from your OpenCode config" : "Requires OpenCode CLI"
7944
8277
  }
7945
8278
  ];
@@ -7961,9 +8294,6 @@ async function runProvidersAdd() {
7961
8294
  p10.cancel("Cancelled.");
7962
8295
  return 0;
7963
8296
  }
7964
- if (choice === "opencode-cloud") {
7965
- return runOpenCodeCloudAddFlow();
7966
- }
7967
8297
  if (choice === "import") {
7968
8298
  if (!hasOpencode) {
7969
8299
  p10.log.error("OpenCode CLI not found. Install from https://opencode.ai");
@@ -8008,6 +8338,9 @@ async function runCloudBuiltinDetail(id) {
8008
8338
  printCloudProviderPanel(name);
8009
8339
  return "back";
8010
8340
  }
8341
+ function providerHubChoiceValue(entry) {
8342
+ return entry.cloudBuiltin ? `cloud:${entry.cloudBuiltin}` : `provider:${entry.id}`;
8343
+ }
8011
8344
  async function runProviderDetail(id) {
8012
8345
  const registry = loadRegistry();
8013
8346
  const provider = registry.providers.find((pr) => pr.id === id);
@@ -8065,23 +8398,24 @@ async function runProvidersHub() {
8065
8398
  const hasOpencode = findOpencodeBinary() !== null;
8066
8399
  while (true) {
8067
8400
  const entries = await resolveProvidersForDisplay();
8068
- const options = [];
8401
+ const options = [
8402
+ { value: "add", label: pc10.bold("+ Add a provider"), hint: "" }
8403
+ ];
8069
8404
  for (const entry of entries) {
8070
8405
  const hint = entry.id;
8071
- const value = `provider:${entry.id}`;
8406
+ const value = providerHubChoiceValue(entry);
8072
8407
  options.push({
8073
8408
  value,
8074
8409
  label: providerLabel(entry.name, entry.modelCount, entry.enabled),
8075
8410
  hint
8076
8411
  });
8077
8412
  }
8078
- options.push({ value: "add", label: "+ Add a provider", hint: "" });
8079
8413
  options.push({ value: "auth-menu", label: "\u2192 Sign in with OAuth (xAI / OpenAI)", hint: "Device code or OpenCode broker" });
8080
8414
  if (entries.length > 0) {
8081
8415
  options.push({ value: "refresh-all", label: "\u21BA Refresh all models", hint: "Update model lists for all providers" });
8082
8416
  }
8083
8417
  if (hasOpencode) {
8084
- options.push({ value: "import", label: "\u2192 Bring settings from OpenCode CLI", hint: "One-time import" });
8418
+ options.push({ value: "import", label: "\u2192 import providers from 'open code CLI'", hint: "One-time import" });
8085
8419
  }
8086
8420
  options.push({ value: "done", label: "Done", hint: "" });
8087
8421
  const choice = await p10.select({
@@ -8291,12 +8625,12 @@ function translateResponsesTools(tools) {
8291
8625
  }
8292
8626
  return Object.keys(out).length ? out : void 0;
8293
8627
  }
8294
- function translateResponsesRequest(body, npm) {
8628
+ function translateResponsesRequest(body, npm, metadata) {
8295
8629
  const { system, messages } = translateResponsesInput(body.input, body.instructions, npm);
8296
8630
  const effort = body.reasoning?.effort;
8297
8631
  const providerOptions = deepMergeProviderOptions(
8298
8632
  thinkingProviderOptions(npm),
8299
- effortProviderOptions(npm, effort, body.model)
8633
+ effortProviderOptions(npm, effort, body.model, metadata)
8300
8634
  );
8301
8635
  return {
8302
8636
  system,
@@ -8338,11 +8672,9 @@ async function writeResponsesStream(fullStream, modelId, write) {
8338
8672
  let textItemId = null;
8339
8673
  let textOutputIndex = 0;
8340
8674
  let textFull = "";
8341
- let toolItemId = null;
8342
- let toolCallId = null;
8343
- let toolName = null;
8344
- let toolOutputIndex = 0;
8345
- let toolArgsFull = "";
8675
+ const toolStates = [];
8676
+ const toolStatesById = /* @__PURE__ */ new Map();
8677
+ let currentToolState = null;
8346
8678
  let reasoningItemId = null;
8347
8679
  let reasoningText = "";
8348
8680
  let reasoningOutputIndex = 0;
@@ -8367,6 +8699,51 @@ async function writeResponsesStream(fullStream, modelId, write) {
8367
8699
  }
8368
8700
  return textItemId;
8369
8701
  };
8702
+ const rememberToolState = (state) => {
8703
+ toolStates.push(state);
8704
+ toolStatesById.set(state.itemId, state);
8705
+ toolStatesById.set(state.callId, state);
8706
+ currentToolState = state;
8707
+ return state;
8708
+ };
8709
+ const createToolState = (rawId, name, signature) => {
8710
+ const itemId = rawId ?? newItemId("fc");
8711
+ const state = rememberToolState({
8712
+ itemId,
8713
+ callId: encodeToolUseId(itemId, signature),
8714
+ name: name ?? "unknown",
8715
+ outputIndex: outputIndex++,
8716
+ args: ""
8717
+ });
8718
+ emit("response.output_item.added", {
8719
+ type: "response.output_item.added",
8720
+ output_index: state.outputIndex,
8721
+ item: {
8722
+ type: "function_call",
8723
+ id: state.itemId,
8724
+ call_id: state.callId,
8725
+ name: state.name,
8726
+ arguments: "",
8727
+ status: "in_progress"
8728
+ }
8729
+ });
8730
+ return state;
8731
+ };
8732
+ const findToolState = (part) => {
8733
+ const key = part.id ?? part.toolCallId;
8734
+ if (key) return toolStatesById.get(key) ?? currentToolState;
8735
+ return currentToolState;
8736
+ };
8737
+ const appendToolArgs = (state, delta) => {
8738
+ if (!delta) return;
8739
+ state.args += delta;
8740
+ emit("response.function_call_arguments.delta", {
8741
+ type: "response.function_call_arguments.delta",
8742
+ item_id: state.itemId,
8743
+ output_index: state.outputIndex,
8744
+ delta
8745
+ });
8746
+ };
8370
8747
  for await (const part of fullStream) {
8371
8748
  switch (part.type) {
8372
8749
  case "reasoning-start":
@@ -8410,64 +8787,20 @@ async function writeResponsesStream(fullStream, modelId, write) {
8410
8787
  break;
8411
8788
  case "tool-input-start": {
8412
8789
  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
- });
8790
+ createToolState(part.id ?? part.toolCallId, part.toolName, sig);
8431
8791
  break;
8432
8792
  }
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
- }
8793
+ case "tool-input-delta": {
8794
+ const state = findToolState(part);
8795
+ if (state) appendToolArgs(state, part.delta ?? part.text ?? "");
8443
8796
  break;
8797
+ }
8444
8798
  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
- });
8799
+ const sig = grabRoundTripSignature(part);
8800
+ const key = part.toolCallId ?? part.id;
8801
+ const state = (key ? toolStatesById.get(key) : void 0) ?? createToolState(key, part.toolName, sig);
8802
+ if (!state.args) {
8803
+ appendToolArgs(state, JSON.stringify(part.input ?? {}));
8471
8804
  }
8472
8805
  break;
8473
8806
  }
@@ -8530,24 +8863,24 @@ async function writeResponsesStream(fullStream, modelId, write) {
8530
8863
  });
8531
8864
  outputItems.unshift(reasoningItem);
8532
8865
  }
8533
- if (toolItemId && toolCallId && toolName) {
8866
+ for (const tool3 of toolStates) {
8534
8867
  emit("response.function_call_arguments.done", {
8535
8868
  type: "response.function_call_arguments.done",
8536
- item_id: toolItemId,
8537
- output_index: toolOutputIndex,
8538
- arguments: toolArgsFull
8869
+ item_id: tool3.itemId,
8870
+ output_index: tool3.outputIndex,
8871
+ arguments: tool3.args
8539
8872
  });
8540
8873
  const fcItem = {
8541
8874
  type: "function_call",
8542
- id: toolItemId,
8543
- call_id: toolCallId,
8544
- name: toolName,
8545
- arguments: toolArgsFull,
8875
+ id: tool3.itemId,
8876
+ call_id: tool3.callId,
8877
+ name: tool3.name,
8878
+ arguments: tool3.args,
8546
8879
  status: "completed"
8547
8880
  };
8548
8881
  emit("response.output_item.done", {
8549
8882
  type: "response.output_item.done",
8550
- output_index: toolOutputIndex,
8883
+ output_index: tool3.outputIndex,
8551
8884
  item: fcItem
8552
8885
  });
8553
8886
  outputItems.push(fcItem);
@@ -8597,10 +8930,11 @@ async function generateResponsesResponse(model, params, modelId) {
8597
8930
  });
8598
8931
  }
8599
8932
  for (const tc of r.toolCalls) {
8933
+ const encodedId = encodeToolUseId(tc.toolCallId, grabRoundTripSignature(tc));
8600
8934
  output.push({
8601
8935
  type: "function_call",
8602
8936
  id: tc.toolCallId,
8603
- call_id: tc.toolCallId,
8937
+ call_id: encodedId,
8604
8938
  name: tc.toolName,
8605
8939
  arguments: JSON.stringify(tc.input ?? {}),
8606
8940
  status: "completed"
@@ -8748,7 +9082,8 @@ async function startCodexProxy(routes, options = {}) {
8748
9082
  modelId: route.upstreamModelId,
8749
9083
  apiKey: route.apiKey,
8750
9084
  baseURL: route.baseURL,
8751
- providerId: route.modelId
9085
+ providerId: route.modelId,
9086
+ vertex: route.vertex
8752
9087
  }));
8753
9088
  }
8754
9089
  return new Promise((resolve, reject2) => {
@@ -8831,7 +9166,14 @@ async function startCodexProxy(routes, options = {}) {
8831
9166
  try {
8832
9167
  const params = translateResponsesRequest(
8833
9168
  body,
8834
- route.npm
9169
+ route.npm,
9170
+ {
9171
+ providerId: route.providerId,
9172
+ apiBaseUrl: route.baseURL,
9173
+ supportedParameters: route.supportedParameters,
9174
+ reasoning: route.reasoning,
9175
+ interleavedReasoningField: route.interleavedReasoningField
9176
+ }
8835
9177
  );
8836
9178
  if (debug) {
8837
9179
  const effort = body.reasoning?.effort;
@@ -8992,13 +9334,8 @@ function isProcessAlive(pid) {
8992
9334
  return false;
8993
9335
  }
8994
9336
  }
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
9337
  function isConcurrentSession(lock) {
9001
- return isProcessAlive(lock.pid) && sessionAgeMs(lock) <= STALE_SESSION_MS;
9338
+ return isProcessAlive(lock.pid);
9002
9339
  }
9003
9340
  function restoreCodexOverlay(env = process.env) {
9004
9341
  const removed = [];
@@ -9041,13 +9378,16 @@ function checkSessionLock(isTty, env = process.env) {
9041
9378
  // src/codex/profile.ts
9042
9379
  var CODEX_LAUNCH_SANDBOX = "danger-full-access";
9043
9380
  function profileReasoningLine(effort) {
9044
- return effort ? `model_reasoning_effort = "${effort}"
9381
+ return effort ? `model_reasoning_effort = ${tomlString(effort)}
9045
9382
  ` : "";
9046
9383
  }
9047
9384
  function profileSandboxLine() {
9048
- return `sandbox = "${CODEX_LAUNCH_SANDBOX}"
9385
+ return `sandbox = ${tomlString(CODEX_LAUNCH_SANDBOX)}
9049
9386
  `;
9050
9387
  }
9388
+ function tomlString(value) {
9389
+ return JSON.stringify(value);
9390
+ }
9051
9391
  function buildCodexProfileToml(spec) {
9052
9392
  const { route, proxyPort, catalogPath, modelReasoningEffort } = spec;
9053
9393
  const model = route.modelId;
@@ -9056,26 +9396,26 @@ function buildCodexProfileToml(spec) {
9056
9396
  const envKey = codexProviderEnvKey(route.providerId);
9057
9397
  const baseUrl = route.baseURL ?? "https://api.openai.com/v1";
9058
9398
  return `# Generated by relay-ai \u2014 do not edit
9059
- ${profileSandboxLine()}model = "${model}"
9060
- model_provider = "${route.providerId}"
9061
- model_catalog_json = "${catalogPath}"
9399
+ ${profileSandboxLine()}model = ${tomlString(model)}
9400
+ model_provider = ${tomlString(route.providerId)}
9401
+ model_catalog_json = ${tomlString(catalogPath)}
9062
9402
  ${reasoning}
9063
9403
  [model_providers.${route.providerId}]
9064
- name = "${route.providerId}"
9065
- base_url = "${baseUrl}"
9066
- env_key = "${envKey}"
9404
+ name = ${tomlString(route.providerId)}
9405
+ base_url = ${tomlString(baseUrl)}
9406
+ env_key = ${tomlString(envKey)}
9067
9407
  wire_api = "responses"
9068
9408
  `;
9069
9409
  }
9070
9410
  const proxyBase = `http://127.0.0.1:${proxyPort}/v1`;
9071
9411
  return `# Generated by relay-ai \u2014 do not edit
9072
- ${profileSandboxLine()}model = "${model}"
9412
+ ${profileSandboxLine()}model = ${tomlString(model)}
9073
9413
  model_provider = "relay-ai-proxy"
9074
- model_catalog_json = "${catalogPath}"
9414
+ model_catalog_json = ${tomlString(catalogPath)}
9075
9415
  ${reasoning}
9076
9416
  [model_providers.relay-ai-proxy]
9077
9417
  name = "relay-ai"
9078
- base_url = "${proxyBase}"
9418
+ base_url = ${tomlString(proxyBase)}
9079
9419
  env_key = "RELAY_AI_CODEX_KEY"
9080
9420
  wire_api = "responses"
9081
9421
  `;
@@ -9340,7 +9680,13 @@ function buildEntry(r, priority) {
9340
9680
  }
9341
9681
  function defaultReasoningEffortForFavorite(r) {
9342
9682
  const model = enrichFavoriteModel(r);
9343
- const caps = getReasoningCapabilities(model.npm ?? "", model.upstreamModelId ?? model.id);
9683
+ const caps = getReasoningCapabilities(model.npm ?? "", model.upstreamModelId ?? model.id, {
9684
+ providerId: r.providerId,
9685
+ apiBaseUrl: model.apiBaseUrl,
9686
+ supportedParameters: model.supportedParameters,
9687
+ reasoning: model.reasoning,
9688
+ interleavedReasoningField: model.interleavedReasoningField
9689
+ });
9344
9690
  return caps.levels.length > 0 ? caps.defaultLevel : "none";
9345
9691
  }
9346
9692
  function buildFavoritesAppCatalog(resolved) {
@@ -9356,65 +9702,6 @@ function buildFavoritesAppCatalog(resolved) {
9356
9702
 
9357
9703
  // src/codex/favorites-launch.ts
9358
9704
  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
9705
  function buildCodexProxyRoutesFromResolved(resolved, providersById) {
9419
9706
  return resolved.map((r) => {
9420
9707
  const provider = providersById.get(r.providerId);
@@ -9650,15 +9937,26 @@ ${pc13.bold("Examples:")}
9650
9937
  ${pc13.bold("Favorites:")}
9651
9938
  When you have saved favorites via ${pc13.cyan("relay-ai models")}, the Codex
9652
9939
  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.`;
9940
+ Zen/Go favorites are included when an OpenCode API key is available.`;
9655
9941
  }
9656
9942
  async function writeLaunchArtifacts(route, selectedModel, providerName, proxyPort) {
9657
9943
  const catalogPath = getCatalogOutputPath(route.providerId);
9658
9944
  const catalog = buildCatalogFile([selectedModel], providerName);
9659
9945
  writeOverlayFile(catalogPath, serializeCatalog(catalog));
9660
9946
  const profilePath = getProfileOutputPath();
9661
- writeOverlayFile(profilePath, buildCodexProfileToml({ route, proxyPort, catalogPath }));
9947
+ const caps = getReasoningCapabilities(route.npm, route.upstreamModelId, {
9948
+ providerId: route.providerId,
9949
+ apiBaseUrl: route.baseURL,
9950
+ supportedParameters: route.supportedParameters,
9951
+ reasoning: route.reasoning,
9952
+ interleavedReasoningField: route.interleavedReasoningField
9953
+ });
9954
+ writeOverlayFile(profilePath, buildCodexProfileToml({
9955
+ route,
9956
+ proxyPort,
9957
+ catalogPath,
9958
+ modelReasoningEffort: caps.defaultLevel || void 0
9959
+ }));
9662
9960
  return { profilePath, catalogPath };
9663
9961
  }
9664
9962
  async function writeFavoritesLaunchArtifacts(resolved, starting, proxyPort) {
@@ -9696,6 +9994,103 @@ function printCodexCleanupReminder(hadProxy) {
9696
9994
  parts.push("If a future session acts stuck: relay-ai codex --restore");
9697
9995
  p13.log.info(parts.join(" "));
9698
9996
  }
9997
+ function vertexEntryToLocalModel(entry) {
9998
+ return {
9999
+ id: entry.id,
10000
+ name: entry.display_name,
10001
+ family: "claude",
10002
+ brand: "Anthropic",
10003
+ modelFormat: "openai",
10004
+ upstreamModelId: entry.upstream_id ?? entry.id,
10005
+ baseUrl: "",
10006
+ npm: VERTEX_ANTHROPIC_NPM,
10007
+ contextWindow: resolveContextWindow(entry.id)
10008
+ };
10009
+ }
10010
+ async function runCodexVertexLaunch(passthroughArgs, trace) {
10011
+ if (!hasApplicationDefaultCredentials()) {
10012
+ p13.log.error("Google Application Default Credentials not found.");
10013
+ p13.log.info("Run: gcloud auth application-default login");
10014
+ return 1;
10015
+ }
10016
+ const config = buildVertexRuntimeConfig();
10017
+ if (!config) {
10018
+ p13.log.error("ANTHROPIC_VERTEX_PROJECT_ID (or GOOGLE_CLOUD_PROJECT) is not set.");
10019
+ p13.log.info("Set your project: export ANTHROPIC_VERTEX_PROJECT_ID=your-project-id");
10020
+ return 1;
10021
+ }
10022
+ let selectedEntry;
10023
+ if (config.models.length === 1) {
10024
+ selectedEntry = config.models[0];
10025
+ } else {
10026
+ const choice = await p13.select({
10027
+ message: "Select a Vertex AI model:",
10028
+ options: config.models.map((m) => ({ value: m, label: m.display_name, hint: m.id }))
10029
+ });
10030
+ if (p13.isCancel(choice)) {
10031
+ p13.cancel("Cancelled.");
10032
+ return 0;
10033
+ }
10034
+ selectedEntry = choice;
10035
+ }
10036
+ process.env["ANTHROPIC_VERTEX_PROJECT_ID"] = config.project;
10037
+ process.env["GOOGLE_CLOUD_LOCATION"] = config.location;
10038
+ const vertexConfig = { project: config.project, location: config.location };
10039
+ const allModels = config.models.map(vertexEntryToLocalModel);
10040
+ const allRoutes = allModels.map((m) => ({
10041
+ modelId: m.id,
10042
+ upstreamModelId: m.upstreamModelId,
10043
+ npm: VERTEX_ANTHROPIC_NPM,
10044
+ apiKey: "",
10045
+ providerId: "vertex",
10046
+ vertex: vertexConfig
10047
+ }));
10048
+ const startingRoute = {
10049
+ tier: "proxy",
10050
+ modelId: selectedEntry.id,
10051
+ upstreamModelId: selectedEntry.upstream_id ?? selectedEntry.id,
10052
+ npm: VERTEX_ANTHROPIC_NPM,
10053
+ apiKey: "",
10054
+ providerId: "vertex"
10055
+ };
10056
+ const debugLogPath = getCodexProxyDebugLogPath();
10057
+ let proxyHandle = null;
10058
+ try {
10059
+ p13.log.info(`Vertex AI \xB7 ${selectedEntry.display_name} \u2014 project: ${config.project} / location: ${config.location}`);
10060
+ proxyHandle = await startCodexProxy(allRoutes, { debug: trace });
10061
+ const proxyPort = proxyHandle.port;
10062
+ const catalogPath = getCatalogOutputPath("vertex");
10063
+ writeOverlayFile(catalogPath, serializeCatalog(buildCatalogFile(allModels, "Vertex AI")));
10064
+ const profilePath = getProfileOutputPath();
10065
+ const caps = getReasoningCapabilities(VERTEX_ANTHROPIC_NPM, selectedEntry.id);
10066
+ writeOverlayFile(profilePath, buildCodexProfileToml({
10067
+ route: startingRoute,
10068
+ proxyPort,
10069
+ catalogPath,
10070
+ modelReasoningEffort: caps.defaultLevel || void 0
10071
+ }));
10072
+ writeSessionLock({
10073
+ pid: process.pid,
10074
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
10075
+ profilePath,
10076
+ catalogPaths: [catalogPath],
10077
+ proxyPort
10078
+ });
10079
+ if (!isAgentStdoutMode()) {
10080
+ logProxy(proxyPort);
10081
+ logActiveModel(selectedEntry.display_name, selectedEntry.id);
10082
+ printCodexCliCleanupPanel("relay-ai codex --restore");
10083
+ }
10084
+ const childEnv = buildCodexChildEnv(startingRoute, proxyPort);
10085
+ const exitCode = await launchCodex(selectedEntry.id, childEnv, passthroughArgs);
10086
+ if (trace) printTraceLog(debugLogPath);
10087
+ printCodexCleanupReminder(true);
10088
+ return exitCode;
10089
+ } finally {
10090
+ proxyHandle?.close();
10091
+ restoreCodexOverlay();
10092
+ }
10093
+ }
9699
10094
  async function runCodexCommand(codexArgs, trace = false, launch = {}) {
9700
10095
  if (codexArgs.includes("--help") || codexArgs.includes("-h")) {
9701
10096
  console.log(codexHelpText());
@@ -9727,6 +10122,21 @@ async function runCodexCommand(codexArgs, trace = false, launch = {}) {
9727
10122
  p13.log.info(`Debug log: ${debugLogPath}`);
9728
10123
  }
9729
10124
  const isTty = Boolean(process.stdin.isTTY);
10125
+ if (launch.vertex) {
10126
+ if (!configOnly) {
10127
+ const sessionCheck = checkSessionLock(isTty);
10128
+ if (!sessionCheck.ok) {
10129
+ if (sessionCheck.reason === "non_tty") {
10130
+ console.error(pc13.red("relay-ai codex --vertex requires an interactive terminal."));
10131
+ return 1;
10132
+ }
10133
+ console.error(pc13.yellow(`Another relay-ai codex session may be running (pid ${sessionCheck.lock.pid}).`));
10134
+ console.error("Run relay-ai codex --restore to clean up, or wait for it to finish.");
10135
+ return 1;
10136
+ }
10137
+ }
10138
+ return runCodexVertexLaunch(passthroughArgs, trace);
10139
+ }
9730
10140
  const prefs = loadPreferences();
9731
10141
  const launchPlan = planLaunchWizard({
9732
10142
  explicit: { providerId: launch.launchProvider, modelId: launch.launchModel },
@@ -9821,9 +10231,17 @@ Error: ${launchPlan.error}
9821
10231
  const pickedProvider = await pickCodexProvider(compatible, prefs, favoritesActive);
9822
10232
  if (!pickedProvider) return 0;
9823
10233
  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);
10234
+ const favoriteProviders = compatible.map((provider) => ({
10235
+ ...provider,
10236
+ models: routableModelsForProvider(provider, "codex")
10237
+ }));
10238
+ const favoriteStart = resolveFirstAvailableFavorite(favorites, favoriteProviders);
10239
+ if (!favoriteStart) {
10240
+ p13.log.warn("No saved Codex favorites are currently available.");
10241
+ return 0;
10242
+ }
10243
+ activeProvider = favoriteStart.provider;
10244
+ selectedModel = favoriteStart.model;
9827
10245
  p13.log.step(`Loaded Favorites Catalog. Starting model: ${selectedModel.name || selectedModel.id} (${activeProvider.name})`);
9828
10246
  } else {
9829
10247
  activeProvider = pickedProvider;
@@ -9895,7 +10313,11 @@ Error: ${launchPlan.error}
9895
10313
  npm: route.npm,
9896
10314
  apiKey: route.apiKey,
9897
10315
  baseURL: route.baseURL,
9898
- upstreamModelId: route.upstreamModelId
10316
+ upstreamModelId: route.upstreamModelId,
10317
+ providerId: route.providerId,
10318
+ supportedParameters: route.supportedParameters,
10319
+ reasoning: route.reasoning,
10320
+ interleavedReasoningField: route.interleavedReasoningField
9899
10321
  }], { debug: trace });
9900
10322
  proxyPort = proxyHandle.port;
9901
10323
  }
@@ -10053,7 +10475,13 @@ function mergeAppConfig(existing, spec) {
10053
10475
  };
10054
10476
  const existingEffort = typeof out.model_reasoning_effort === "string" ? out.model_reasoning_effort : void 0;
10055
10477
  if (existingEffort !== void 0) {
10056
- const caps = getReasoningCapabilities(spec.route.npm, spec.route.modelId);
10478
+ const caps = getReasoningCapabilities(spec.route.npm, spec.route.modelId, {
10479
+ providerId: spec.route.providerId,
10480
+ apiBaseUrl: spec.route.baseURL,
10481
+ supportedParameters: spec.route.supportedParameters,
10482
+ reasoning: spec.route.reasoning,
10483
+ interleavedReasoningField: spec.route.interleavedReasoningField
10484
+ });
10057
10485
  if (caps.levels.length === 0 || !caps.levels.includes(existingEffort)) {
10058
10486
  if (caps.levels.length > 0 && caps.defaultLevel) {
10059
10487
  out.model_reasoning_effort = caps.defaultLevel;
@@ -10243,7 +10671,7 @@ function removeAppCatalogs(env = process.env) {
10243
10671
  }
10244
10672
  function restoreCodexAppOverlay(env = process.env) {
10245
10673
  const lock = readAppSessionLock(env);
10246
- if (lock && isConcurrentSession(lock)) {
10674
+ if (lock && lock.pid !== process.pid && isConcurrentSession(lock)) {
10247
10675
  return {
10248
10676
  restored: false,
10249
10677
  liveSession: true,
@@ -10272,7 +10700,7 @@ function recoverInterruptedCodexAppSession(env = process.env) {
10272
10700
  const lock = readAppSessionLock(env);
10273
10701
  const managed = isAppManagedConfig(readCodexConfigText());
10274
10702
  if (!lock && !managed) return { recovered: false };
10275
- if (lock && isConcurrentSession(lock)) {
10703
+ if (lock && lock.pid !== process.pid && isConcurrentSession(lock)) {
10276
10704
  return { recovered: false };
10277
10705
  }
10278
10706
  restoreCodexAppOverlay(env);
@@ -10281,7 +10709,7 @@ function recoverInterruptedCodexAppSession(env = process.env) {
10281
10709
  function checkAppSessionLock(isTty, env = process.env) {
10282
10710
  if (!isTty) return { ok: false, reason: "non_tty" };
10283
10711
  const lock = readAppSessionLock(env);
10284
- if (lock && isConcurrentSession(lock)) {
10712
+ if (lock && lock.pid !== process.pid && isConcurrentSession(lock)) {
10285
10713
  return { ok: false, reason: "concurrent", lock };
10286
10714
  }
10287
10715
  return { ok: true };
@@ -10547,13 +10975,154 @@ ${pc14.bold("Examples:")}
10547
10975
  ${pc14.bold("Favorites:")}
10548
10976
  When you have saved favorites via ${pc14.cyan("relay-ai models")}, the Codex App
10549
10977
  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.`;
10978
+ Zen/Go favorites are included when an OpenCode API key is available.`;
10552
10979
  }
10553
10980
  function providerForCodexPicker(provider) {
10554
10981
  return { ...provider, models: routableModelsForProvider(provider, "codex-app") };
10555
10982
  }
10556
- async function runCodexAppCommand(args) {
10983
+ function vertexEntryToLocalModel2(entry) {
10984
+ return {
10985
+ id: entry.id,
10986
+ name: entry.display_name,
10987
+ family: "claude",
10988
+ brand: "Anthropic",
10989
+ modelFormat: "openai",
10990
+ upstreamModelId: entry.upstream_id ?? entry.id,
10991
+ baseUrl: "",
10992
+ npm: VERTEX_ANTHROPIC_NPM,
10993
+ contextWindow: resolveContextWindow(entry.id)
10994
+ };
10995
+ }
10996
+ async function runCodexAppVertexLaunch(configOnly) {
10997
+ if (!hasApplicationDefaultCredentials()) {
10998
+ p15.log.error("Google Application Default Credentials not found.");
10999
+ p15.log.info("Run: gcloud auth application-default login");
11000
+ return 1;
11001
+ }
11002
+ const config = buildVertexRuntimeConfig();
11003
+ if (!config) {
11004
+ p15.log.error("ANTHROPIC_VERTEX_PROJECT_ID (or GOOGLE_CLOUD_PROJECT) is not set.");
11005
+ p15.log.info("Set your project: export ANTHROPIC_VERTEX_PROJECT_ID=your-project-id");
11006
+ return 1;
11007
+ }
11008
+ let selectedEntry;
11009
+ if (config.models.length === 1) {
11010
+ selectedEntry = config.models[0];
11011
+ } else {
11012
+ const choice = await p15.select({
11013
+ message: "Select a starting Vertex AI model:",
11014
+ options: config.models.map((m) => ({ value: m, label: m.display_name, hint: m.id }))
11015
+ });
11016
+ if (p15.isCancel(choice)) {
11017
+ p15.cancel("Cancelled.");
11018
+ return 0;
11019
+ }
11020
+ selectedEntry = choice;
11021
+ }
11022
+ process.env["ANTHROPIC_VERTEX_PROJECT_ID"] = config.project;
11023
+ process.env["GOOGLE_CLOUD_LOCATION"] = config.location;
11024
+ const vertexConfig = { project: config.project, location: config.location };
11025
+ const vertexModels = config.models.map(vertexEntryToLocalModel2);
11026
+ const catalogPath = getAppCatalogPath("vertex");
11027
+ const route = {
11028
+ tier: "proxy",
11029
+ modelId: selectedEntry.id,
11030
+ upstreamModelId: selectedEntry.upstream_id ?? selectedEntry.id,
11031
+ npm: VERTEX_ANTHROPIC_NPM,
11032
+ apiKey: "",
11033
+ providerId: "vertex"
11034
+ };
11035
+ if (configOnly) {
11036
+ const home = process.env["HOME"] ?? "";
11037
+ const shortenPath = (fp) => home ? fp.replace(home, "~") : fp;
11038
+ console.log("");
11039
+ console.log(pc14.bold(pc14.cyan(" CONFIG PREVIEW \u2014 relay-ai codex-app --vertex")));
11040
+ console.log("");
11041
+ console.log(` ${pc14.bold("Mode:")} Vertex AI`);
11042
+ console.log(` ${pc14.bold("Project:")} ${config.project}`);
11043
+ console.log(` ${pc14.bold("Location:")} ${config.location}`);
11044
+ console.log(` ${pc14.bold("Model:")} ${selectedEntry.display_name}`);
11045
+ console.log(` ${pc14.bold("Catalog:")} ${vertexModels.length} model${vertexModels.length !== 1 ? "s" : ""} available`);
11046
+ console.log("");
11047
+ console.log(` ${pc14.bold("Catalog file:")}`);
11048
+ console.log(` ${pc14.dim(shortenPath(catalogPath))}`);
11049
+ console.log("");
11050
+ console.log(pc14.dim(" No app was launched."));
11051
+ console.log(pc14.dim(" Run ") + pc14.cyan("relay-ai codex-app --vertex") + pc14.dim(" to launch."));
11052
+ console.log("");
11053
+ return 0;
11054
+ }
11055
+ let proxyHandle = null;
11056
+ let sessionActive = false;
11057
+ try {
11058
+ proxyHandle = await startCodexProxy(
11059
+ vertexModels.map((m) => ({
11060
+ modelId: m.id,
11061
+ upstreamModelId: m.upstreamModelId,
11062
+ npm: VERTEX_ANTHROPIC_NPM,
11063
+ apiKey: "",
11064
+ providerId: "vertex",
11065
+ vertex: vertexConfig
11066
+ })),
11067
+ { requireAuth: false }
11068
+ );
11069
+ const proxyPort = proxyHandle.port;
11070
+ const catalogFile = buildAppCatalogFile(vertexModels, "Vertex AI", selectedEntry.id);
11071
+ writeOverlayFile(catalogPath, serializeCatalog(catalogFile));
11072
+ const spec = {
11073
+ route,
11074
+ proxyPort,
11075
+ catalogPath,
11076
+ providerDisplayName: `${selectedEntry.display_name} \xB7 Vertex AI`
11077
+ };
11078
+ saveAppRestoreStateBeforePatch();
11079
+ const backupPath = backupConfigToml();
11080
+ applyAppConfigPatch(spec);
11081
+ writeAppSessionLock({
11082
+ pid: process.pid,
11083
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
11084
+ configPath: getCodexConfigPath(),
11085
+ catalogPaths: [catalogPath],
11086
+ restoreStatePath: getAppRestoreStatePath(),
11087
+ backupPath,
11088
+ proxyPort
11089
+ });
11090
+ sessionActive = true;
11091
+ p15.log.info(`Vertex AI \xB7 ${selectedEntry.display_name} \u2014 project: ${config.project} / location: ${config.location}`);
11092
+ logProxy(proxyPort);
11093
+ logActiveModel(selectedEntry.display_name, selectedEntry.id);
11094
+ try {
11095
+ await launchOrRestartCodexApp();
11096
+ } catch (err) {
11097
+ p15.log.warn(String(err instanceof Error ? err.message : err));
11098
+ p15.log.info(codexAppInstallHint());
11099
+ }
11100
+ printCodexAppSessionPanel({
11101
+ modelLabel: selectedEntry.display_name,
11102
+ modelId: selectedEntry.id,
11103
+ providerName: "Vertex AI",
11104
+ restoreCommand: "relay-ai codex-app --restore"
11105
+ });
11106
+ codexAppOutro(selectedEntry.display_name);
11107
+ await waitForShutdown2();
11108
+ console.log("");
11109
+ if (sessionActive) {
11110
+ restoreCodexAppOverlay();
11111
+ sessionActive = false;
11112
+ }
11113
+ if (isCodexAppRunning()) {
11114
+ const shouldClose = await p15.confirm({ message: "Codex Desktop is still running. Close it?" });
11115
+ if (shouldClose && !p15.isCancel(shouldClose)) {
11116
+ quitCodexAppGracefully();
11117
+ }
11118
+ }
11119
+ return 0;
11120
+ } finally {
11121
+ proxyHandle?.close();
11122
+ if (sessionActive) restoreCodexAppOverlay();
11123
+ }
11124
+ }
11125
+ async function runCodexAppCommand(args, opts = {}) {
10557
11126
  if (args.includes("--help") || args.includes("-h")) {
10558
11127
  console.log(codexAppHelpText());
10559
11128
  return 0;
@@ -10590,6 +11159,9 @@ async function runCodexAppCommand(args) {
10590
11159
  p15.log.warn("Recovered from an interrupted codex-app session (restored Codex config).");
10591
11160
  }
10592
11161
  }
11162
+ if (opts.vertex) {
11163
+ return runCodexAppVertexLaunch(configOnly);
11164
+ }
10593
11165
  const catalogSpinner = p15.spinner();
10594
11166
  catalogSpinner.start("Loading your providers...");
10595
11167
  let catalog;
@@ -10626,9 +11198,14 @@ async function runCodexAppCommand(args) {
10626
11198
  const pickedProvider = await pickCodexProvider(compatible, prefs, favoritesActive);
10627
11199
  if (!pickedProvider) return 0;
10628
11200
  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);
11201
+ const favoriteProviders = compatible.map(providerForCodexPicker);
11202
+ const favoriteStart = resolveFirstAvailableFavorite(favorites, favoriteProviders);
11203
+ if (!favoriteStart) {
11204
+ p15.log.warn("No saved Codex App favorites are currently available.");
11205
+ return 0;
11206
+ }
11207
+ activeProvider = favoriteStart.provider;
11208
+ selectedModel = favoriteStart.model;
10632
11209
  p15.log.step(`Loaded Favorites Catalog. Starting model: ${selectedModel.name || selectedModel.id} (${activeProvider.name})`);
10633
11210
  } else {
10634
11211
  activeProvider = providerForCodexPicker(pickedProvider);
@@ -11203,6 +11780,8 @@ function claudeAppHelpText() {
11203
11780
 
11204
11781
  ${pc15.bold("Usage:")}
11205
11782
  relay-ai claude-app
11783
+ relay-ai claude-app --trace
11784
+ relay-ai claude-app --restore
11206
11785
 
11207
11786
  ${pc15.bold("Description:")}
11208
11787
  Picks a provider and model from ~/.relay-ai/providers.json, patches Claude Desktop config
@@ -11214,6 +11793,7 @@ ${pc15.bold("Platforms:")}
11214
11793
 
11215
11794
  ${pc15.bold("Cleanup:")}
11216
11795
  Ctrl+C stops the proxy and restores your previous Claude config.
11796
+ After a crash: relay-ai claude-app --restore
11217
11797
  `;
11218
11798
  }
11219
11799
  function providerForClaudePicker(provider) {
@@ -11224,6 +11804,14 @@ async function runClaudeAppCommand(args) {
11224
11804
  console.log(claudeAppHelpText());
11225
11805
  return 0;
11226
11806
  }
11807
+ if (args.includes("--restore")) {
11808
+ recoverSession();
11809
+ console.log("Restored Claude Desktop relay-ai config.");
11810
+ return 0;
11811
+ }
11812
+ const trace = args.includes("--trace");
11813
+ const debugLogPath = trace ? getProxyDebugLogPath() : void 0;
11814
+ if (trace) console.log(`Debug log: ${debugLogPath}`);
11227
11815
  try {
11228
11816
  claudeAppSupported();
11229
11817
  } catch (err) {
@@ -11321,7 +11909,8 @@ async function runClaudeAppCommand(args) {
11321
11909
  serverPassword: null,
11322
11910
  catalog: createGatewayModelCatalog(serverModels, { maskGatewayIds: true }),
11323
11911
  backends: BACKENDS,
11324
- gateway: { maskGatewayIds: true }
11912
+ gateway: { maskGatewayIds: true },
11913
+ debugLogPath
11325
11914
  });
11326
11915
  const uuid = writeRelayAiConfig(proxyHandle.port);
11327
11916
  writeSessionLock2({
@@ -11792,7 +12381,7 @@ PROVIDER / MODEL DISCOVERY FOR ALEF CONFIG
11792
12381
  5. relay-ai --ai (includes live state section at bottom of output)
11793
12382
 
11794
12383
  ALEF CHECKLIST
11795
- \u25A1 relay-ai on PATH (npm install -g relay-ai; dev: npm link after builds)
12384
+ \u25A1 relay-ai on PATH (npm install -g @jacobbd/relay-ai; dev: npm link after builds)
11796
12385
  \u25A1 Always pass --provider + --model (or provider__model slug) \u2014 never rely on wizard
11797
12386
  \u25A1 Claude: --output-format stream-json (or json) with -p
11798
12387
  \u25A1 Codex: exec --json (not bare codex exec without --json if parsing stdout)
@@ -11998,6 +12587,7 @@ function parseArgs(args) {
11998
12587
  for (const arg of rest) {
11999
12588
  if (arg === "--help" || arg === "-h") parsed2.showHelp = true;
12000
12589
  else if (arg === "--version" || arg === "-v") parsed2.showVersion = true;
12590
+ else if (arg === "--vertex") parsed2.vertex = true;
12001
12591
  }
12002
12592
  return parsed2;
12003
12593
  }
@@ -12018,6 +12608,10 @@ function parseArgs(args) {
12018
12608
  parsed2.trace = true;
12019
12609
  continue;
12020
12610
  }
12611
+ if (arg === "--vertex") {
12612
+ parsed2.vertex = true;
12613
+ continue;
12614
+ }
12021
12615
  if (arg === "--help" || arg === "-h") {
12022
12616
  parsed2.showHelp = true;
12023
12617
  continue;
@@ -12175,7 +12769,7 @@ ${pc16.bold("Behavior:")}
12175
12769
  ${pc16.bold("Vertex env:")}
12176
12770
  ANTHROPIC_VERTEX_PROJECT_ID or GOOGLE_CLOUD_PROJECT \u2014 your GCP project
12177
12771
  GOOGLE_CLOUD_LOCATION or CLOUD_ML_REGION \u2014 region (default: global)
12178
- Optional catalog: ~/.relay-ai/vertex-models.json (see vertex-models.example.json)
12772
+ Optional catalog: ~/.relay-ai/vertex-models.json (see assets/vertex-models.example.json)
12179
12773
 
12180
12774
  ${pc16.bold("Endpoints:")}
12181
12775
  Anthropic-compatible: ANTHROPIC_BASE_URL=http://127.0.0.1:17645/anthropic
@@ -12461,9 +13055,13 @@ Error: ${launchPlan.error}
12461
13055
  }
12462
13056
  const providerChoice = chosen;
12463
13057
  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);
13058
+ const favoriteStart = resolveFirstAvailableFavorite(favorites, allProviders);
13059
+ if (!favoriteStart) {
13060
+ p18.log.warn("No saved favorites are currently available.");
13061
+ return 0;
13062
+ }
13063
+ activeProvider = favoriteStart.provider;
13064
+ selectedModel = favoriteStart.model;
12467
13065
  p18.log.step(`Loaded Favorites Catalog. Starting model: ${selectedModel.name || selectedModel.id} (${activeProvider.name})`);
12468
13066
  } else {
12469
13067
  activeProvider = allProviders.find((lp) => lp.id === providerChoice);
@@ -12559,7 +13157,11 @@ Error: ${launchPlan.error}
12559
13157
  {
12560
13158
  npm: selectedModel.npm,
12561
13159
  baseURL: selectedModel.apiBaseUrl,
12562
- upstreamModelId: selectedModel.upstreamModelId
13160
+ upstreamModelId: selectedModel.upstreamModelId,
13161
+ providerId: activeProvider.id,
13162
+ supportedParameters: selectedModel.supportedParameters,
13163
+ reasoning: selectedModel.reasoning,
13164
+ interleavedReasoningField: selectedModel.interleavedReasoningField
12563
13165
  }
12564
13166
  );
12565
13167
  if (!isAgentStdoutMode()) {
@@ -12659,7 +13261,7 @@ Error: ${parsed.error}
12659
13261
  console.log(VERSION);
12660
13262
  return 0;
12661
13263
  }
12662
- return runCodexAppCommand(parsed.claudeArgs);
13264
+ return runCodexAppCommand(parsed.claudeArgs, { vertex: parsed.vertex });
12663
13265
  }
12664
13266
  if (parsed.command === "claude-app") {
12665
13267
  if (parsed.showVersion) {
@@ -12679,7 +13281,8 @@ Error: ${parsed.error}
12679
13281
  }
12680
13282
  return runCodexCommand(parsed.claudeArgs, parsed.trace, {
12681
13283
  launchProvider: parsed.launchProvider,
12682
- launchModel: parsed.launchModel
13284
+ launchModel: parsed.launchModel,
13285
+ vertex: parsed.vertex
12683
13286
  });
12684
13287
  }
12685
13288
  if (parsed.showVersion) {