@jacobbd/relay-ai 0.2.2 → 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.2";
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;
@@ -3099,6 +3250,17 @@ function zenRegistryStub(subscriptionFilter) {
3099
3250
  addedAt: (/* @__PURE__ */ new Date()).toISOString()
3100
3251
  };
3101
3252
  }
3253
+ function goRegistryStub() {
3254
+ return {
3255
+ id: "go",
3256
+ templateId: "go",
3257
+ name: "OpenCode Go",
3258
+ enabled: true,
3259
+ authRef: "keyring:global:opencode",
3260
+ api: {},
3261
+ addedAt: (/* @__PURE__ */ new Date()).toISOString()
3262
+ };
3263
+ }
3102
3264
 
3103
3265
  // src/registry/convert.ts
3104
3266
  function modelToCached(model) {
@@ -3112,7 +3274,10 @@ function modelToCached(model) {
3112
3274
  cost: model.cost,
3113
3275
  modelFormat: model.modelFormat,
3114
3276
  npm: model.npm,
3115
- apiUrl: model.apiBaseUrl
3277
+ apiUrl: model.apiBaseUrl,
3278
+ supportedParameters: model.supportedParameters,
3279
+ reasoning: model.reasoning,
3280
+ interleavedReasoningField: model.interleavedReasoningField
3116
3281
  };
3117
3282
  }
3118
3283
  function localProviderToRegistry(provider, opts) {
@@ -3282,7 +3447,8 @@ function parseModelList(body, npm) {
3282
3447
  brand: deriveBrand(family),
3283
3448
  contextWindow: resolveContextWindow(id),
3284
3449
  modelFormat: format,
3285
- npm
3450
+ npm,
3451
+ supportedParameters: Array.isArray(row.supported_parameters) ? row.supported_parameters : void 0
3286
3452
  });
3287
3453
  }
3288
3454
  return models;
@@ -3807,6 +3973,24 @@ var PROVIDER_TEMPLATES = [
3807
3973
  supported: false,
3808
3974
  unsupportedReason: "Uses gcloud Application Default Credentials \u2014 not supported via API key import."
3809
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
+ },
3810
3994
  // OAuth-gated subscription providers — use relay-ai providers auth <id> to sign in
3811
3995
  {
3812
3996
  id: "github-copilot",
@@ -3936,8 +4120,20 @@ async function validateImportKey(lp, entry) {
3936
4120
  }
3937
4121
  return reject("invalid-key", "No API base URL \u2014 cannot verify key.");
3938
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
+ }
3939
4135
  if (npm === "@ai-sdk/anthropic") {
3940
- const result2 = await fetchAnthropicModels(baseUrl, key);
4136
+ const result2 = await fetchAnthropicModels(safeBaseUrl, key);
3941
4137
  if (result2.error) {
3942
4138
  return reject(
3943
4139
  placeholder ? "placeholder-key" : "invalid-key",
@@ -3946,8 +4142,8 @@ async function validateImportKey(lp, entry) {
3946
4142
  }
3947
4143
  return { canImport: true };
3948
4144
  }
3949
- const template = catalogTemplate ?? syntheticTemplate(entry, baseUrl);
3950
- const result = await fetchTemplateModels(template, key, baseUrl);
4145
+ const template = catalogTemplate ?? syntheticTemplate(entry, safeBaseUrl);
4146
+ const result = await fetchTemplateModels(template, key, safeBaseUrl);
3951
4147
  if (result.error) {
3952
4148
  return reject(
3953
4149
  placeholder ? "placeholder-key" : "invalid-key",
@@ -4057,6 +4253,11 @@ async function importFromOpencode(options = {}) {
4057
4253
  continue;
4058
4254
  }
4059
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
+ }
4060
4261
  if (existingIdx >= 0) {
4061
4262
  registry.providers[existingIdx] = { ...entry, addedAt: registry.providers[existingIdx].addedAt };
4062
4263
  } else {
@@ -4064,11 +4265,8 @@ async function importFromOpencode(options = {}) {
4064
4265
  }
4065
4266
  imported.push(entry);
4066
4267
  importedIds.add(lp.id);
4067
- const saved = isOAuth ? await saveOAuthKey(lp.id, oauth) : await saveProviderKey(lp);
4068
- if (saved) {
4069
- keysSaved += 1;
4070
- if (isOAuth) oauthImported += 1;
4071
- }
4268
+ keysSaved += 1;
4269
+ if (isOAuth) oauthImported += 1;
4072
4270
  }
4073
4271
  const alreadyReportedIds = new Set(skipped.map((s) => s.id));
4074
4272
  const registryProviderIds = new Set(registry.providers.map((p19) => p19.id));
@@ -4569,10 +4767,13 @@ function upstreamModelId(model) {
4569
4767
  const id = model.upstreamModelId ?? model.id;
4570
4768
  return id.replace(/\[1m\]$/i, "");
4571
4769
  }
4770
+ function isOpenAIChatCompletionsModel(model) {
4771
+ return model.modelFormat === "openai" && (!!model.completionsUrl || model.sourceBackend === "zen" || model.sourceBackend === "go");
4772
+ }
4572
4773
  function formatOpenAIModels(models) {
4573
4774
  return {
4574
4775
  object: "list",
4575
- data: models.map((model) => ({
4776
+ data: models.filter(isOpenAIChatCompletionsModel).map((model) => ({
4576
4777
  id: model.id,
4577
4778
  object: "model",
4578
4779
  created: CREATED_AT_UNIX,
@@ -4604,20 +4805,24 @@ function extractBearerToken(value) {
4604
4805
  }
4605
4806
 
4606
4807
  // src/upstream-forward.ts
4607
- function anthropicUpstreamHeaders(apiKey, stream = false) {
4808
+ function anthropicUpstreamHeaders(apiKey, stream = false, inboundBeta) {
4608
4809
  const key = sanitizeCredential(apiKey) ?? apiKey.trim();
4609
- return {
4810
+ const headers = {
4610
4811
  "Content-Type": "application/json",
4611
4812
  "anthropic-version": "2023-06-01",
4612
4813
  Authorization: `Bearer ${key}`,
4613
4814
  "x-api-key": key,
4614
4815
  ...stream ? { Accept: "text/event-stream" } : {}
4615
4816
  };
4817
+ if (inboundBeta) {
4818
+ headers["anthropic-beta"] = inboundBeta;
4819
+ }
4820
+ return headers;
4616
4821
  }
4617
- async function postJsonUpstream(url, body, apiKey) {
4822
+ async function postJsonUpstream(url, body, apiKey, inboundBeta) {
4618
4823
  const response = await fetch(url, {
4619
4824
  method: "POST",
4620
- headers: anthropicUpstreamHeaders(apiKey, false),
4825
+ headers: anthropicUpstreamHeaders(apiKey, false, inboundBeta),
4621
4826
  body: JSON.stringify(body)
4622
4827
  });
4623
4828
  const text5 = await response.text();
@@ -4637,12 +4842,12 @@ var UpstreamUnreachableError = class extends Error {
4637
4842
  this.name = "UpstreamUnreachableError";
4638
4843
  }
4639
4844
  };
4640
- async function relayAnthropicMessages(res, messagesUrl, body, apiKey, clientWantsStream) {
4845
+ async function relayAnthropicMessages(res, messagesUrl, body, apiKey, clientWantsStream, inboundBeta) {
4641
4846
  let upstreamRes;
4642
4847
  try {
4643
4848
  upstreamRes = await fetch(messagesUrl, {
4644
4849
  method: "POST",
4645
- headers: anthropicUpstreamHeaders(apiKey, clientWantsStream),
4850
+ headers: anthropicUpstreamHeaders(apiKey, clientWantsStream, inboundBeta),
4646
4851
  body: JSON.stringify(body)
4647
4852
  });
4648
4853
  } catch (err) {
@@ -4668,20 +4873,19 @@ async function relayAnthropicMessages(res, messagesUrl, body, apiKey, clientWant
4668
4873
  res.end(JSON.stringify({ type: "error", error: { type: "api_error", message: "Upstream returned empty response body" } }));
4669
4874
  return;
4670
4875
  }
4671
- let json;
4876
+ const text5 = await upstreamRes.text();
4672
4877
  try {
4673
- json = await upstreamRes.json();
4878
+ JSON.parse(text5);
4674
4879
  } catch {
4675
4880
  res.writeHead(502, { "Content-Type": "application/json" });
4676
4881
  res.end(JSON.stringify({ type: "error", error: { type: "api_error", message: "Upstream response was not valid JSON" } }));
4677
4882
  return;
4678
4883
  }
4679
- const payload = JSON.stringify(json);
4680
4884
  res.writeHead(200, {
4681
4885
  "Content-Type": "application/json",
4682
- "Content-Length": Buffer.byteLength(payload).toString()
4886
+ "Content-Length": Buffer.byteLength(text5).toString()
4683
4887
  });
4684
- res.end(payload);
4888
+ res.end(text5);
4685
4889
  }
4686
4890
 
4687
4891
  // src/sdk-adapter.ts
@@ -4698,7 +4902,7 @@ function silenceSdkWarnings() {
4698
4902
  sdkWarningsSilenced = true;
4699
4903
  globalThis.AI_SDK_LOG_WARNINGS = false;
4700
4904
  }
4701
- var TOOL_USE_SIG_SEP = "::ts::";
4905
+ var TOOL_USE_SIG_SEP = "__ts__";
4702
4906
  function parseToolArguments(value) {
4703
4907
  if (value === null || value === void 0) return {};
4704
4908
  if (typeof value === "object" && !Array.isArray(value)) return value;
@@ -4721,15 +4925,26 @@ data: ${JSON.stringify(data)}
4721
4925
  `;
4722
4926
  }
4723
4927
  function splitToolUseId(id) {
4724
- const sep = id.lastIndexOf(TOOL_USE_SIG_SEP);
4725
- if (sep === -1) return { rawId: id };
4726
- return {
4727
- rawId: id.slice(0, sep),
4728
- thoughtSignature: id.slice(sep + TOOL_USE_SIG_SEP.length)
4729
- };
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 };
4730
4943
  }
4731
4944
  function encodeToolUseId(rawId, thoughtSignature) {
4732
- 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}`;
4733
4948
  }
4734
4949
  function serializeToolResultContent(content) {
4735
4950
  return typeof content === "string" ? content : JSON.stringify(content);
@@ -4931,7 +5146,7 @@ function translateRequest(body, npm, options) {
4931
5146
  const effort = anthropicEffortFromRequest(body) ?? options?.defaultEffort;
4932
5147
  const providerOptions = deepMergeProviderOptions(
4933
5148
  thinkingProviderOptions(npm),
4934
- effortProviderOptions(npm, effort, body.model)
5149
+ effortProviderOptions(npm, effort, body.model, options?.reasoningMetadata)
4935
5150
  );
4936
5151
  return {
4937
5152
  system,
@@ -5114,7 +5329,7 @@ async function generateAnthropicResponse(model, params, modelId) {
5114
5329
  ...r.text ? [{ type: "text", text: r.text }] : [],
5115
5330
  ...r.toolCalls.map((tc) => ({
5116
5331
  type: "tool_use",
5117
- id: tc.toolCallId,
5332
+ id: encodeToolUseId(tc.toolCallId, grabRoundTripSignature(tc)),
5118
5333
  name: tc.toolName,
5119
5334
  input: tc.input
5120
5335
  }))
@@ -5240,11 +5455,13 @@ function startProxyCatalog(routes, defaultAliasId, debug = false) {
5240
5455
  return;
5241
5456
  }
5242
5457
  if (route.modelFormat === "anthropic") {
5458
+ const betaHeaderRaw = req.headers["anthropic-beta"];
5459
+ const inboundBeta = Array.isArray(betaHeaderRaw) ? betaHeaderRaw.join(",") : betaHeaderRaw;
5243
5460
  const forwardBody = { ...anthropicBody, model: route.realModelId };
5244
5461
  const targetUrl = `${upstreamUrl}/v1/messages`;
5245
5462
  plog(() => `anthropic-passthrough: model=${route.realModelId}, stream=${clientWantsStream}`);
5246
5463
  try {
5247
- await relayAnthropicMessages(res, targetUrl, forwardBody, apiKey, clientWantsStream);
5464
+ await relayAnthropicMessages(res, targetUrl, forwardBody, apiKey, clientWantsStream, inboundBeta);
5248
5465
  } catch (err) {
5249
5466
  const message = err instanceof UpstreamUnreachableError ? err.message : String(err);
5250
5467
  plog(() => `anthropic-passthrough error: ${message}`);
@@ -5253,7 +5470,15 @@ function startProxyCatalog(routes, defaultAliasId, debug = false) {
5253
5470
  return;
5254
5471
  }
5255
5472
  if (isSdkMigratedNpm(route.npm)) {
5256
- 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
+ });
5257
5482
  plog(
5258
5483
  () => `sdk: npm=${route.npm} model=${route.realModelId}, stream=${clientWantsStream}, tools=${anthropicBody.tools?.length ?? 0}, msgs=${params.messages.length}`
5259
5484
  );
@@ -5330,7 +5555,11 @@ function startProxy(completionsUrl, modelId, debug = false, contextWindow, sdk)
5330
5555
  modelFormat: "openai",
5331
5556
  contextWindow,
5332
5557
  npm: sdk?.npm,
5333
- baseURL: sdk?.baseURL
5558
+ baseURL: sdk?.baseURL,
5559
+ providerId: sdk?.providerId,
5560
+ supportedParameters: sdk?.supportedParameters,
5561
+ reasoning: sdk?.reasoning,
5562
+ interleavedReasoningField: sdk?.interleavedReasoningField
5334
5563
  }], clientModelId, debug);
5335
5564
  }
5336
5565
 
@@ -5347,7 +5576,11 @@ function localModelToRoute(lp, model) {
5347
5576
  modelFormat: model.modelFormat,
5348
5577
  contextWindow: model.contextWindow,
5349
5578
  npm: model.npm,
5350
- baseURL: model.apiBaseUrl
5579
+ baseURL: model.apiBaseUrl,
5580
+ providerId: lp.id,
5581
+ supportedParameters: model.supportedParameters,
5582
+ reasoning: model.reasoning,
5583
+ interleavedReasoningField: model.interleavedReasoningField
5351
5584
  };
5352
5585
  }
5353
5586
  function zenGoModelToRoute(model, apiKey) {
@@ -5365,7 +5598,8 @@ function zenGoModelToRoute(model, apiKey) {
5365
5598
  // openai-format Zen/Go models route through the SDK (openai-compatible);
5366
5599
  // anthropic models stay direct passthrough (no npm).
5367
5600
  npm: isAnthropic ? void 0 : "@ai-sdk/openai-compatible",
5368
- baseURL: isAnthropic ? void 0 : `${backend.baseUrl}/v1`
5601
+ baseURL: isAnthropic ? void 0 : `${backend.baseUrl}/v1`,
5602
+ providerId: model.sourceBackend
5369
5603
  };
5370
5604
  }
5371
5605
  function makeRouteResolver(localProviders, zenModels, goModels, zenGoApiKey) {
@@ -5535,6 +5769,7 @@ function cachedModelToLocal(cached, provider) {
5535
5769
  const apiUrl = cached.apiUrl ?? provider.api.url ?? "";
5536
5770
  const endpoint = resolveEndpoint(npm, apiUrl);
5537
5771
  if (endpoint === null) return null;
5772
+ const modelsDev = findModelsDevModel(provider.id, cached.id);
5538
5773
  const { id, upstreamModelId: upstreamModelId2 } = normalizeGoogleModelId(cached.id, npm);
5539
5774
  const normalizedUpstream = normalizeGoogleModelId(cached.upstreamModelId ?? cached.id, npm).upstreamModelId;
5540
5775
  const family = npm === "@ai-sdk/google" ? id.split(/[-/:]/)[0] ?? id : cached.family ?? "";
@@ -5550,7 +5785,10 @@ function cachedModelToLocal(cached, provider) {
5550
5785
  npm: npm || void 0,
5551
5786
  apiBaseUrl: apiUrl || void 0,
5552
5787
  cost: cached.cost,
5553
- 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
5554
5792
  };
5555
5793
  }
5556
5794
  function materializeOne(provider, resolveCredential, agent) {
@@ -5751,7 +5989,10 @@ function localProvidersToServerModels(localProviders) {
5751
5989
  npm: model.modelFormat === "openai" ? model.npm || "@ai-sdk/openai-compatible" : model.npm,
5752
5990
  apiBaseUrl: model.apiBaseUrl,
5753
5991
  apiKey: provider.apiKey,
5754
- contextWindow: model.contextWindow
5992
+ contextWindow: model.contextWindow,
5993
+ supportedParameters: model.supportedParameters,
5994
+ reasoning: model.reasoning,
5995
+ interleavedReasoningField: model.interleavedReasoningField
5755
5996
  }))
5756
5997
  );
5757
5998
  }
@@ -5874,11 +6115,18 @@ async function askSaveServerPassword() {
5874
6115
 
5875
6116
  // src/server/router.ts
5876
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
+ }
5877
6124
  async function startServer(options) {
5878
6125
  silenceSdkWarnings();
5879
6126
  const languageModelCache = /* @__PURE__ */ new Map();
6127
+ const plog = makeServerLog(options.debugLogPath);
5880
6128
  const server = createServer2((req, res) => {
5881
- void routeRequest(req, res, options, languageModelCache);
6129
+ void routeRequest(req, res, options, languageModelCache, plog);
5882
6130
  });
5883
6131
  await new Promise((resolve, reject2) => {
5884
6132
  server.once("error", reject2);
@@ -5901,9 +6149,10 @@ async function startServer(options) {
5901
6149
  })
5902
6150
  };
5903
6151
  }
5904
- async function routeRequest(req, res, options, modelCache) {
6152
+ async function routeRequest(req, res, options, modelCache, plog) {
5905
6153
  try {
5906
6154
  const pathname = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`).pathname;
6155
+ plog(`${req.method} ${pathname}`);
5907
6156
  if (req.method === "GET" && pathname === "/health") {
5908
6157
  sendJson(res, 200, { ok: true });
5909
6158
  return;
@@ -5925,7 +6174,7 @@ async function routeRequest(req, res, options, modelCache) {
5925
6174
  return;
5926
6175
  }
5927
6176
  if (req.method === "POST" && pathname === "/anthropic/v1/messages") {
5928
- await handleAnthropicMessages(req, res, options, modelCache);
6177
+ await handleAnthropicMessages(req, res, options, modelCache, plog);
5929
6178
  return;
5930
6179
  }
5931
6180
  if (req.method === "POST" && pathname === "/openai/v1/chat/completions") {
@@ -5937,14 +6186,18 @@ async function routeRequest(req, res, options, modelCache) {
5937
6186
  sendJson(res, 500, { error: { message: err instanceof Error ? err.message : String(err) } });
5938
6187
  }
5939
6188
  }
5940
- async function handleAnthropicMessages(req, res, options, modelCache) {
6189
+ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
5941
6190
  const body = await readJson(req);
5942
6191
  if (!body) {
5943
6192
  sendJson(res, 400, { error: { message: "Invalid JSON body" } });
5944
6193
  return;
5945
6194
  }
5946
6195
  const model = lookupModel(res, options.catalog, body.model);
5947
- 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}`);
5948
6201
  if (model.modelFormat === "anthropic") {
5949
6202
  if (model.baseUrl && !/^https?:\/\//i.test(model.baseUrl)) {
5950
6203
  sendJson(res, 400, { error: { message: `Invalid provider baseUrl: must be http:// or https://` } });
@@ -5952,7 +6205,10 @@ async function handleAnthropicMessages(req, res, options, modelCache) {
5952
6205
  }
5953
6206
  const messagesUrl = model.baseUrl ? `${model.baseUrl}/v1/messages` : `${backendFor(options, model).baseUrl}/v1/messages`;
5954
6207
  const apiKey = model.apiKey ?? options.apiKey;
5955
- 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);
5956
6212
  return;
5957
6213
  }
5958
6214
  if (model.modelFormat === "openai") {
@@ -5961,22 +6217,32 @@ async function handleAnthropicMessages(req, res, options, modelCache) {
5961
6217
  return;
5962
6218
  }
5963
6219
  const apiKey = model.apiKey ?? options.apiKey;
5964
- let languageModel = modelCache.get(model.id);
6220
+ const cacheKey = sdkModelCacheKey(model);
6221
+ let languageModel = modelCache.get(cacheKey);
5965
6222
  if (!languageModel) {
5966
6223
  languageModel = await createLanguageModel({
5967
6224
  npm: model.npm,
5968
6225
  modelId: upstreamModelId(model),
5969
6226
  apiKey,
5970
6227
  baseURL: model.apiBaseUrl,
5971
- providerId: model.sourceBackend,
6228
+ providerId: model.providerId ?? model.sourceBackend,
5972
6229
  vertex: options.vertex
5973
6230
  });
5974
- modelCache.set(model.id, languageModel);
6231
+ modelCache.set(cacheKey, languageModel);
5975
6232
  }
5976
6233
  const params = translateRequest(body, model.npm, {
5977
- 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
+ }
5978
6242
  });
5979
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}`);
5980
6246
  try {
5981
6247
  if (clientWantsStream) {
5982
6248
  res.writeHead(200, {
@@ -5984,12 +6250,10 @@ async function handleAnthropicMessages(req, res, options, modelCache) {
5984
6250
  "Cache-Control": "no-cache",
5985
6251
  "Connection": "keep-alive"
5986
6252
  });
5987
- const clientModel = typeof body.model === "string" ? body.model : model.id;
5988
- await streamAnthropicResponse(languageModel, params, clientModel, (chunk) => res.write(chunk));
6253
+ await streamAnthropicResponse(languageModel, params, responseModelId, (chunk) => res.write(chunk));
5989
6254
  res.end();
5990
6255
  } else {
5991
- const clientModel = typeof body.model === "string" ? body.model : model.id;
5992
- const anthropicResponse = await generateAnthropicResponse(languageModel, params, clientModel);
6256
+ const anthropicResponse = await generateAnthropicResponse(languageModel, params, responseModelId);
5993
6257
  sendJson(res, 200, anthropicResponse);
5994
6258
  }
5995
6259
  } catch (err) {
@@ -6010,6 +6274,14 @@ async function handleOpenAIChatCompletions(req, res, options) {
6010
6274
  const model = lookupModel(res, options.catalog, body.model);
6011
6275
  if (!model) return;
6012
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
+ }
6013
6285
  if (model.completionsUrl && !/^https?:\/\//i.test(model.completionsUrl)) {
6014
6286
  sendJson(res, 400, { error: { message: `Invalid provider completionsUrl: must be http:// or https://` } });
6015
6287
  return;
@@ -6045,15 +6317,22 @@ function backendFor(options, model) {
6045
6317
  if (model.sourceBackend === "go") return options.backends.go;
6046
6318
  throw new Error(`Provider ${model.sourceBackend} is not a cloud backend \u2014 model must set baseUrl/completionsUrl`);
6047
6319
  }
6048
- async function forwardJson(res, url, body, apiKey) {
6049
- 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);
6050
6331
  sendJson(res, upstream.status, upstream.body);
6051
6332
  }
6052
6333
  async function readJson(req) {
6053
6334
  try {
6054
- const chunks = [];
6055
- for await (const chunk of req) chunks.push(Buffer.from(chunk));
6056
- const raw = Buffer.concat(chunks).toString();
6335
+ const raw = await readBody(req);
6057
6336
  return raw ? JSON.parse(raw) : {};
6058
6337
  } catch {
6059
6338
  return null;
@@ -6209,7 +6488,9 @@ function resolveVertexLocation(env = process.env) {
6209
6488
  function defaultAdcCredentialsPath(home = homedir7()) {
6210
6489
  return join10(home, ".config", "gcloud", "application_default_credentials.json");
6211
6490
  }
6212
- 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;
6213
6494
  return existsSync10(adcPath);
6214
6495
  }
6215
6496
  function loadVertexModelEntries(env = process.env) {
@@ -6240,19 +6521,23 @@ function buildVertexRuntimeConfig(env = process.env) {
6240
6521
  };
6241
6522
  }
6242
6523
  function vertexModelsToServerModels(config) {
6243
- return config.models.map((model) => ({
6244
- id: model.id,
6245
- name: model.display_name,
6246
- isFree: false,
6247
- brand: "Anthropic",
6248
- sourceBackend: "vertex",
6249
- modelFormat: "openai",
6250
- upstreamModelId: model.upstream_id ?? model.id,
6251
- npm: VERTEX_ANTHROPIC_NPM,
6252
- providerLabel: "Vertex AI",
6253
- providerId: "vertex",
6254
- contextWindow: resolveContextWindow(model.id)
6255
- }));
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
+ });
6256
6541
  }
6257
6542
  function vertexClientModelLookupCandidates(modelId) {
6258
6543
  const candidates = [modelId];
@@ -6369,7 +6654,13 @@ async function loadServerModels() {
6369
6654
  }
6370
6655
  function enrichServerModelReasoning(model) {
6371
6656
  if (!model.npm || model.modelFormat !== "openai") return model;
6372
- 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
+ });
6373
6664
  if (!caps.defaultLevel) return model;
6374
6665
  return { ...model, defaultEffort: caps.defaultLevel };
6375
6666
  }
@@ -6955,6 +7246,71 @@ async function pickGlobalFavoriteModel(providers, favorites) {
6955
7246
  }
6956
7247
  }
6957
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
+
6958
7314
  // src/providers-command.ts
6959
7315
  import pc10 from "picocolors";
6960
7316
  import * as p10 from "@clack/prompts";
@@ -7071,6 +7427,24 @@ async function removeProviderFromRegistry(id, opts) {
7071
7427
  credentialDeleted
7072
7428
  };
7073
7429
  }
7430
+ function addZenRegistryStub(opts) {
7431
+ const registry = loadRegistry();
7432
+ if (registry.providers.some((p19) => p19.id === "zen")) {
7433
+ return { added: false, reason: "OpenCode Zen is already configured." };
7434
+ }
7435
+ registry.providers.push(zenRegistryStub(opts?.subscriptionFilter));
7436
+ saveRegistry(registry);
7437
+ return { added: true };
7438
+ }
7439
+ function addGoRegistryStub() {
7440
+ const registry = loadRegistry();
7441
+ if (registry.providers.some((p19) => p19.id === "go")) {
7442
+ return { added: false, reason: "OpenCode Go is already configured." };
7443
+ }
7444
+ registry.providers.push(goRegistryStub());
7445
+ saveRegistry(registry);
7446
+ return { added: true };
7447
+ }
7074
7448
  function toggleProviderEnabled(id) {
7075
7449
  const registry = loadRegistry();
7076
7450
  const provider = registry.providers.find((p19) => p19.id === id);
@@ -7099,18 +7473,35 @@ function modelInfoToCached(m, npm, apiUrl) {
7099
7473
  async function refreshZenGoProvider(provider) {
7100
7474
  const backendId = provider.id === "go" || provider.templateId === "go" ? "go" : "zen";
7101
7475
  const result = await getModels(BACKENDS[backendId]);
7102
- 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
+ });
7103
7482
  }
7104
7483
  async function refreshApiListProvider(provider, apiKey) {
7105
7484
  const npm = provider.api.npm ?? "@ai-sdk/openai-compatible";
7106
7485
  const catalogTemplate = resolveProviderTemplate(provider);
7107
7486
  const baseUrl = effectiveProviderBaseUrl(provider, catalogTemplate);
7108
- const template = catalogTemplate ?? syntheticTemplate(provider, baseUrl);
7109
7487
  if (!baseUrl) {
7110
7488
  return { models: [], error: "Provider has no API base URL configured." };
7111
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);
7112
7503
  if (npm === "@ai-sdk/anthropic") {
7113
- const fetched2 = await fetchAnthropicModels(baseUrl, apiKey);
7504
+ const fetched2 = await fetchAnthropicModels(safeBaseUrl, apiKey);
7114
7505
  if (fetched2.error || fetched2.models.length === 0) {
7115
7506
  return { models: [], error: fetched2.error ?? "No models returned.", baseUrl: fetched2.baseUrl };
7116
7507
  }
@@ -7119,7 +7510,7 @@ async function refreshApiListProvider(provider, apiKey) {
7119
7510
  baseUrl: fetched2.baseUrl
7120
7511
  };
7121
7512
  }
7122
- const fetched = await fetchTemplateModels(template, apiKey, baseUrl);
7513
+ const fetched = await fetchTemplateModels(template, apiKey, safeBaseUrl);
7123
7514
  if (fetched.error || fetched.models.length === 0) {
7124
7515
  return { models: [], error: fetched.error ?? "No models returned." };
7125
7516
  }
@@ -7169,7 +7560,10 @@ async function refreshProviderModels(providerId, apiKey, registry = loadRegistry
7169
7560
  if (source === "zen-go-api") {
7170
7561
  models = await refreshZenGoProvider(provider);
7171
7562
  } else {
7172
- 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)) {
7173
7567
  if (cachedModelCount(provider) > 0) {
7174
7568
  return skipWithCachedModels(
7175
7569
  provider,
@@ -7183,7 +7577,7 @@ async function refreshProviderModels(providerId, apiKey, registry = loadRegistry
7183
7577
  reason: "No usable API key \u2014 add the provider via relay-ai providers add with a real key."
7184
7578
  };
7185
7579
  }
7186
- if (!apiKey) {
7580
+ if (!keyOptional && !effectiveKey) {
7187
7581
  return {
7188
7582
  id: provider.id,
7189
7583
  name: provider.name,
@@ -7191,7 +7585,7 @@ async function refreshProviderModels(providerId, apiKey, registry = loadRegistry
7191
7585
  reason: "API key not available \u2014 cannot refresh models."
7192
7586
  };
7193
7587
  }
7194
- const fetched = await refreshApiListProvider(provider, apiKey);
7588
+ const fetched = await refreshApiListProvider(provider, effectiveKey ?? "");
7195
7589
  if (fetched.error) {
7196
7590
  if ((fetched.error.includes("rejected") || fetched.error.includes("401") || fetched.error.includes("403")) && cachedModelCount(provider) > 0) {
7197
7591
  return skipWithCachedModels(
@@ -7388,7 +7782,10 @@ async function authenticateProvider(providerId, options = {}) {
7388
7782
  if (!supportsNativeOAuth(providerId)) {
7389
7783
  if (findOpencodeBinary()) {
7390
7784
  const cred2 = await runOpencodeAuthBroker(providerId, { method: options.brokerMethod });
7391
- 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
+ }
7392
7789
  const registryProvider2 = await upsertOAuthProvider(providerId, cred2);
7393
7790
  return { providerId, credential: cred2, registryProvider: registryProvider2 };
7394
7791
  }
@@ -7510,7 +7907,7 @@ ${pc10.bold("Usage:")}
7510
7907
  ${pc10.bold("Subcommands:")}
7511
7908
  (none) Provider hub wizard ${pc10.dim("[Phase 1.1]")}
7512
7909
  add Add a provider (Groq, Mistral, Together AI, \u2026) ${pc10.dim("[Phase 1.1]")}
7513
- 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]")}
7514
7911
  auth Sign in with OAuth (xAI, OpenAI ChatGPT) ${pc10.dim("[Phase 2]")}
7515
7912
  list Show configured providers ${pc10.dim("[Phase 1.0]")}
7516
7913
  remove Remove a provider by id ${pc10.dim("[Phase 1.1]")}
@@ -7559,7 +7956,7 @@ async function runProvidersImport() {
7559
7956
  );
7560
7957
  if (result.skipped.length > 0) {
7561
7958
  for (const s of result.skipped) {
7562
- 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;
7563
7960
  p10.log.warn(`Skipped ${s.name} (${s.id}): ${reason}`);
7564
7961
  }
7565
7962
  }
@@ -7570,6 +7967,18 @@ async function runProvidersImport() {
7570
7967
  }
7571
7968
  }
7572
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
+ }
7573
7982
  return 0;
7574
7983
  }
7575
7984
  async function runProvidersAuth(providerId, method) {
@@ -7659,7 +8068,9 @@ async function runProvidersList() {
7659
8068
  }
7660
8069
  async function pickTemplateFromCatalog() {
7661
8070
  while (true) {
7662
- 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);
7663
8074
  if (templates.length === 0) return null;
7664
8075
  const method = await p10.select({
7665
8076
  message: `Choose a provider (${templates.length} available)`,
@@ -7687,9 +8098,15 @@ async function pickTemplateFromCatalog() {
7687
8098
  placeholder: "e.g. groq, mistral, openrouter"
7688
8099
  });
7689
8100
  if (p10.isCancel(searchInput)) continue;
7690
- const matched = filterTemplates(templates, String(searchInput));
8101
+ const query = String(searchInput);
8102
+ const matched = filterTemplates(templates, query);
7691
8103
  if (matched.length === 0) {
7692
- p10.log.warn("No providers match \u2014 try a different search");
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");
8109
+ }
7693
8110
  continue;
7694
8111
  }
7695
8112
  const options = matched.map((t) => ({
@@ -7713,6 +8130,40 @@ async function runTemplateAddFlow() {
7713
8130
  }
7714
8131
  const template = await pickTemplateFromCatalog();
7715
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
+ }
7716
8167
  if (template.signupUrl) {
7717
8168
  printPanel(fmtProvider(template.name), [
7718
8169
  `${pc10.white("Get an API key at:")} ${fmtUrl(template.signupUrl)}`
@@ -7819,7 +8270,11 @@ async function runProvidersAdd() {
7819
8270
  const registry = loadRegistry();
7820
8271
  const hasOpencode = findOpencodeBinary() !== null;
7821
8272
  const options = [
7822
- { value: "import", label: "Bring settings from OpenCode", hint: hasOpencode ? "One-time import" : "Requires OpenCode CLI" }
8273
+ {
8274
+ value: "import",
8275
+ label: "import providers from 'open code CLI'",
8276
+ hint: hasOpencode ? "Import Groq, OpenAI, etc. from your OpenCode config" : "Requires OpenCode CLI"
8277
+ }
7823
8278
  ];
7824
8279
  const addableTemplates = listAddableTemplates(registry.providers.map((p19) => p19.id));
7825
8280
  if (addableTemplates.length > 0) {
@@ -7883,6 +8338,9 @@ async function runCloudBuiltinDetail(id) {
7883
8338
  printCloudProviderPanel(name);
7884
8339
  return "back";
7885
8340
  }
8341
+ function providerHubChoiceValue(entry) {
8342
+ return entry.cloudBuiltin ? `cloud:${entry.cloudBuiltin}` : `provider:${entry.id}`;
8343
+ }
7886
8344
  async function runProviderDetail(id) {
7887
8345
  const registry = loadRegistry();
7888
8346
  const provider = registry.providers.find((pr) => pr.id === id);
@@ -7940,23 +8398,24 @@ async function runProvidersHub() {
7940
8398
  const hasOpencode = findOpencodeBinary() !== null;
7941
8399
  while (true) {
7942
8400
  const entries = await resolveProvidersForDisplay();
7943
- const options = [];
8401
+ const options = [
8402
+ { value: "add", label: pc10.bold("+ Add a provider"), hint: "" }
8403
+ ];
7944
8404
  for (const entry of entries) {
7945
8405
  const hint = entry.id;
7946
- const value = `provider:${entry.id}`;
8406
+ const value = providerHubChoiceValue(entry);
7947
8407
  options.push({
7948
8408
  value,
7949
8409
  label: providerLabel(entry.name, entry.modelCount, entry.enabled),
7950
8410
  hint
7951
8411
  });
7952
8412
  }
7953
- options.push({ value: "add", label: "+ Add a provider", hint: "" });
7954
8413
  options.push({ value: "auth-menu", label: "\u2192 Sign in with OAuth (xAI / OpenAI)", hint: "Device code or OpenCode broker" });
7955
8414
  if (entries.length > 0) {
7956
8415
  options.push({ value: "refresh-all", label: "\u21BA Refresh all models", hint: "Update model lists for all providers" });
7957
8416
  }
7958
8417
  if (hasOpencode) {
7959
- options.push({ value: "import", label: "\u2192 Bring settings from OpenCode", hint: "One-time import" });
8418
+ options.push({ value: "import", label: "\u2192 import providers from 'open code CLI'", hint: "One-time import" });
7960
8419
  }
7961
8420
  options.push({ value: "done", label: "Done", hint: "" });
7962
8421
  const choice = await p10.select({
@@ -8166,12 +8625,12 @@ function translateResponsesTools(tools) {
8166
8625
  }
8167
8626
  return Object.keys(out).length ? out : void 0;
8168
8627
  }
8169
- function translateResponsesRequest(body, npm) {
8628
+ function translateResponsesRequest(body, npm, metadata) {
8170
8629
  const { system, messages } = translateResponsesInput(body.input, body.instructions, npm);
8171
8630
  const effort = body.reasoning?.effort;
8172
8631
  const providerOptions = deepMergeProviderOptions(
8173
8632
  thinkingProviderOptions(npm),
8174
- effortProviderOptions(npm, effort, body.model)
8633
+ effortProviderOptions(npm, effort, body.model, metadata)
8175
8634
  );
8176
8635
  return {
8177
8636
  system,
@@ -8213,11 +8672,9 @@ async function writeResponsesStream(fullStream, modelId, write) {
8213
8672
  let textItemId = null;
8214
8673
  let textOutputIndex = 0;
8215
8674
  let textFull = "";
8216
- let toolItemId = null;
8217
- let toolCallId = null;
8218
- let toolName = null;
8219
- let toolOutputIndex = 0;
8220
- let toolArgsFull = "";
8675
+ const toolStates = [];
8676
+ const toolStatesById = /* @__PURE__ */ new Map();
8677
+ let currentToolState = null;
8221
8678
  let reasoningItemId = null;
8222
8679
  let reasoningText = "";
8223
8680
  let reasoningOutputIndex = 0;
@@ -8242,6 +8699,51 @@ async function writeResponsesStream(fullStream, modelId, write) {
8242
8699
  }
8243
8700
  return textItemId;
8244
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
+ };
8245
8747
  for await (const part of fullStream) {
8246
8748
  switch (part.type) {
8247
8749
  case "reasoning-start":
@@ -8285,64 +8787,20 @@ async function writeResponsesStream(fullStream, modelId, write) {
8285
8787
  break;
8286
8788
  case "tool-input-start": {
8287
8789
  const sig = grabRoundTripSignature(part);
8288
- toolItemId = part.id ?? newItemId("fc");
8289
- toolCallId = encodeToolUseId(toolItemId, sig);
8290
- toolName = part.toolName ?? "unknown";
8291
- toolArgsFull = "";
8292
- toolOutputIndex = outputIndex;
8293
- outputIndex++;
8294
- emit("response.output_item.added", {
8295
- type: "response.output_item.added",
8296
- output_index: toolOutputIndex,
8297
- item: {
8298
- type: "function_call",
8299
- id: toolItemId,
8300
- call_id: toolCallId,
8301
- name: toolName,
8302
- arguments: "",
8303
- status: "in_progress"
8304
- }
8305
- });
8790
+ createToolState(part.id ?? part.toolCallId, part.toolName, sig);
8306
8791
  break;
8307
8792
  }
8308
- case "tool-input-delta":
8309
- toolArgsFull += part.delta ?? part.text ?? "";
8310
- if (toolItemId) {
8311
- emit("response.function_call_arguments.delta", {
8312
- type: "response.function_call_arguments.delta",
8313
- item_id: toolItemId,
8314
- output_index: toolOutputIndex,
8315
- delta: part.delta ?? part.text ?? ""
8316
- });
8317
- }
8793
+ case "tool-input-delta": {
8794
+ const state = findToolState(part);
8795
+ if (state) appendToolArgs(state, part.delta ?? part.text ?? "");
8318
8796
  break;
8797
+ }
8319
8798
  case "tool-call": {
8320
- if (!toolItemId) {
8321
- const sig = grabRoundTripSignature(part);
8322
- toolItemId = part.toolCallId ?? newItemId("fc");
8323
- toolCallId = encodeToolUseId(toolItemId, sig);
8324
- toolName = part.toolName ?? "unknown";
8325
- toolArgsFull = JSON.stringify(part.input ?? {});
8326
- toolOutputIndex = outputIndex;
8327
- outputIndex++;
8328
- emit("response.output_item.added", {
8329
- type: "response.output_item.added",
8330
- output_index: toolOutputIndex,
8331
- item: {
8332
- type: "function_call",
8333
- id: toolItemId,
8334
- call_id: toolCallId,
8335
- name: toolName,
8336
- arguments: "",
8337
- status: "in_progress"
8338
- }
8339
- });
8340
- emit("response.function_call_arguments.delta", {
8341
- type: "response.function_call_arguments.delta",
8342
- item_id: toolItemId,
8343
- output_index: toolOutputIndex,
8344
- delta: toolArgsFull
8345
- });
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 ?? {}));
8346
8804
  }
8347
8805
  break;
8348
8806
  }
@@ -8405,24 +8863,24 @@ async function writeResponsesStream(fullStream, modelId, write) {
8405
8863
  });
8406
8864
  outputItems.unshift(reasoningItem);
8407
8865
  }
8408
- if (toolItemId && toolCallId && toolName) {
8866
+ for (const tool3 of toolStates) {
8409
8867
  emit("response.function_call_arguments.done", {
8410
8868
  type: "response.function_call_arguments.done",
8411
- item_id: toolItemId,
8412
- output_index: toolOutputIndex,
8413
- arguments: toolArgsFull
8869
+ item_id: tool3.itemId,
8870
+ output_index: tool3.outputIndex,
8871
+ arguments: tool3.args
8414
8872
  });
8415
8873
  const fcItem = {
8416
8874
  type: "function_call",
8417
- id: toolItemId,
8418
- call_id: toolCallId,
8419
- name: toolName,
8420
- arguments: toolArgsFull,
8875
+ id: tool3.itemId,
8876
+ call_id: tool3.callId,
8877
+ name: tool3.name,
8878
+ arguments: tool3.args,
8421
8879
  status: "completed"
8422
8880
  };
8423
8881
  emit("response.output_item.done", {
8424
8882
  type: "response.output_item.done",
8425
- output_index: toolOutputIndex,
8883
+ output_index: tool3.outputIndex,
8426
8884
  item: fcItem
8427
8885
  });
8428
8886
  outputItems.push(fcItem);
@@ -8472,10 +8930,11 @@ async function generateResponsesResponse(model, params, modelId) {
8472
8930
  });
8473
8931
  }
8474
8932
  for (const tc of r.toolCalls) {
8933
+ const encodedId = encodeToolUseId(tc.toolCallId, grabRoundTripSignature(tc));
8475
8934
  output.push({
8476
8935
  type: "function_call",
8477
8936
  id: tc.toolCallId,
8478
- call_id: tc.toolCallId,
8937
+ call_id: encodedId,
8479
8938
  name: tc.toolName,
8480
8939
  arguments: JSON.stringify(tc.input ?? {}),
8481
8940
  status: "completed"
@@ -8623,7 +9082,8 @@ async function startCodexProxy(routes, options = {}) {
8623
9082
  modelId: route.upstreamModelId,
8624
9083
  apiKey: route.apiKey,
8625
9084
  baseURL: route.baseURL,
8626
- providerId: route.modelId
9085
+ providerId: route.modelId,
9086
+ vertex: route.vertex
8627
9087
  }));
8628
9088
  }
8629
9089
  return new Promise((resolve, reject2) => {
@@ -8706,7 +9166,14 @@ async function startCodexProxy(routes, options = {}) {
8706
9166
  try {
8707
9167
  const params = translateResponsesRequest(
8708
9168
  body,
8709
- 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
+ }
8710
9177
  );
8711
9178
  if (debug) {
8712
9179
  const effort = body.reasoning?.effort;
@@ -8867,13 +9334,8 @@ function isProcessAlive(pid) {
8867
9334
  return false;
8868
9335
  }
8869
9336
  }
8870
- function sessionAgeMs(lock) {
8871
- const started = Date.parse(lock.startedAt);
8872
- if (Number.isNaN(started)) return Infinity;
8873
- return Date.now() - started;
8874
- }
8875
9337
  function isConcurrentSession(lock) {
8876
- return isProcessAlive(lock.pid) && sessionAgeMs(lock) <= STALE_SESSION_MS;
9338
+ return isProcessAlive(lock.pid);
8877
9339
  }
8878
9340
  function restoreCodexOverlay(env = process.env) {
8879
9341
  const removed = [];
@@ -8916,13 +9378,16 @@ function checkSessionLock(isTty, env = process.env) {
8916
9378
  // src/codex/profile.ts
8917
9379
  var CODEX_LAUNCH_SANDBOX = "danger-full-access";
8918
9380
  function profileReasoningLine(effort) {
8919
- return effort ? `model_reasoning_effort = "${effort}"
9381
+ return effort ? `model_reasoning_effort = ${tomlString(effort)}
8920
9382
  ` : "";
8921
9383
  }
8922
9384
  function profileSandboxLine() {
8923
- return `sandbox = "${CODEX_LAUNCH_SANDBOX}"
9385
+ return `sandbox = ${tomlString(CODEX_LAUNCH_SANDBOX)}
8924
9386
  `;
8925
9387
  }
9388
+ function tomlString(value) {
9389
+ return JSON.stringify(value);
9390
+ }
8926
9391
  function buildCodexProfileToml(spec) {
8927
9392
  const { route, proxyPort, catalogPath, modelReasoningEffort } = spec;
8928
9393
  const model = route.modelId;
@@ -8931,26 +9396,26 @@ function buildCodexProfileToml(spec) {
8931
9396
  const envKey = codexProviderEnvKey(route.providerId);
8932
9397
  const baseUrl = route.baseURL ?? "https://api.openai.com/v1";
8933
9398
  return `# Generated by relay-ai \u2014 do not edit
8934
- ${profileSandboxLine()}model = "${model}"
8935
- model_provider = "${route.providerId}"
8936
- model_catalog_json = "${catalogPath}"
9399
+ ${profileSandboxLine()}model = ${tomlString(model)}
9400
+ model_provider = ${tomlString(route.providerId)}
9401
+ model_catalog_json = ${tomlString(catalogPath)}
8937
9402
  ${reasoning}
8938
9403
  [model_providers.${route.providerId}]
8939
- name = "${route.providerId}"
8940
- base_url = "${baseUrl}"
8941
- env_key = "${envKey}"
9404
+ name = ${tomlString(route.providerId)}
9405
+ base_url = ${tomlString(baseUrl)}
9406
+ env_key = ${tomlString(envKey)}
8942
9407
  wire_api = "responses"
8943
9408
  `;
8944
9409
  }
8945
9410
  const proxyBase = `http://127.0.0.1:${proxyPort}/v1`;
8946
9411
  return `# Generated by relay-ai \u2014 do not edit
8947
- ${profileSandboxLine()}model = "${model}"
9412
+ ${profileSandboxLine()}model = ${tomlString(model)}
8948
9413
  model_provider = "relay-ai-proxy"
8949
- model_catalog_json = "${catalogPath}"
9414
+ model_catalog_json = ${tomlString(catalogPath)}
8950
9415
  ${reasoning}
8951
9416
  [model_providers.relay-ai-proxy]
8952
9417
  name = "relay-ai"
8953
- base_url = "${proxyBase}"
9418
+ base_url = ${tomlString(proxyBase)}
8954
9419
  env_key = "RELAY_AI_CODEX_KEY"
8955
9420
  wire_api = "responses"
8956
9421
  `;
@@ -9215,7 +9680,13 @@ function buildEntry(r, priority) {
9215
9680
  }
9216
9681
  function defaultReasoningEffortForFavorite(r) {
9217
9682
  const model = enrichFavoriteModel(r);
9218
- 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
+ });
9219
9690
  return caps.levels.length > 0 ? caps.defaultLevel : "none";
9220
9691
  }
9221
9692
  function buildFavoritesAppCatalog(resolved) {
@@ -9231,65 +9702,6 @@ function buildFavoritesAppCatalog(resolved) {
9231
9702
 
9232
9703
  // src/codex/favorites-launch.ts
9233
9704
  import * as p12 from "@clack/prompts";
9234
-
9235
- // src/favorites-resolver.ts
9236
- var ZEN_GO_PROVIDER_NAME = {
9237
- zen: "OpenCode Zen",
9238
- go: "OpenCode Go"
9239
- };
9240
- function resolveFavorite(fav, ctx) {
9241
- if (fav.providerId === "zen" || fav.providerId === "go") {
9242
- if (!ctx.zenGoApiKey) return void 0;
9243
- const models = fav.providerId === "zen" ? ctx.zenModels : ctx.goModels;
9244
- const model = models?.find((m) => m.id === fav.modelId);
9245
- if (!model) return void 0;
9246
- return {
9247
- providerId: fav.providerId,
9248
- providerName: ZEN_GO_PROVIDER_NAME[fav.providerId],
9249
- model,
9250
- apiKey: ctx.zenGoApiKey,
9251
- sourceBackend: fav.providerId
9252
- };
9253
- }
9254
- if (ctx.findLocalModel) {
9255
- const found = ctx.findLocalModel(fav.providerId, fav.modelId);
9256
- if (!found) return void 0;
9257
- if (ctx.agent && shouldHideModel({ providerId: fav.providerId, modelId: fav.modelId, agent: ctx.agent })) {
9258
- return void 0;
9259
- }
9260
- return {
9261
- providerId: fav.providerId,
9262
- providerName: found.provider.name,
9263
- model: found.model,
9264
- apiKey: found.provider.apiKey
9265
- };
9266
- }
9267
- return void 0;
9268
- }
9269
- function buildFavoritesList(starting, favorites, ctx, max = 20) {
9270
- const droppedFavorites = [];
9271
- const seen = /* @__PURE__ */ new Set();
9272
- const out = [];
9273
- if (starting) {
9274
- seen.add(`${starting.providerId}::${starting.model.id}`);
9275
- out.push(starting);
9276
- }
9277
- for (const fav of favorites) {
9278
- if (out.length >= max) break;
9279
- const key = `${fav.providerId}::${fav.modelId}`;
9280
- if (seen.has(key)) continue;
9281
- const resolved = resolveFavorite(fav, ctx);
9282
- if (!resolved) {
9283
- droppedFavorites.push(fav);
9284
- continue;
9285
- }
9286
- seen.add(key);
9287
- out.push(resolved);
9288
- }
9289
- return { resolved: out, droppedFavorites };
9290
- }
9291
-
9292
- // src/codex/favorites-launch.ts
9293
9705
  function buildCodexProxyRoutesFromResolved(resolved, providersById) {
9294
9706
  return resolved.map((r) => {
9295
9707
  const provider = providersById.get(r.providerId);
@@ -9525,15 +9937,26 @@ ${pc13.bold("Examples:")}
9525
9937
  ${pc13.bold("Favorites:")}
9526
9938
  When you have saved favorites via ${pc13.cyan("relay-ai models")}, the Codex
9527
9939
  picker will show your starting model + favorites for mid-session switching.
9528
- Zen/Go favorites are skipped \u2014 use ${pc13.cyan("relay-ai claude")} or
9529
- ${pc13.cyan("relay-ai server")} for those.`;
9940
+ Zen/Go favorites are included when an OpenCode API key is available.`;
9530
9941
  }
9531
9942
  async function writeLaunchArtifacts(route, selectedModel, providerName, proxyPort) {
9532
9943
  const catalogPath = getCatalogOutputPath(route.providerId);
9533
9944
  const catalog = buildCatalogFile([selectedModel], providerName);
9534
9945
  writeOverlayFile(catalogPath, serializeCatalog(catalog));
9535
9946
  const profilePath = getProfileOutputPath();
9536
- 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
+ }));
9537
9960
  return { profilePath, catalogPath };
9538
9961
  }
9539
9962
  async function writeFavoritesLaunchArtifacts(resolved, starting, proxyPort) {
@@ -9571,6 +9994,103 @@ function printCodexCleanupReminder(hadProxy) {
9571
9994
  parts.push("If a future session acts stuck: relay-ai codex --restore");
9572
9995
  p13.log.info(parts.join(" "));
9573
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
+ }
9574
10094
  async function runCodexCommand(codexArgs, trace = false, launch = {}) {
9575
10095
  if (codexArgs.includes("--help") || codexArgs.includes("-h")) {
9576
10096
  console.log(codexHelpText());
@@ -9602,6 +10122,21 @@ async function runCodexCommand(codexArgs, trace = false, launch = {}) {
9602
10122
  p13.log.info(`Debug log: ${debugLogPath}`);
9603
10123
  }
9604
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
+ }
9605
10140
  const prefs = loadPreferences();
9606
10141
  const launchPlan = planLaunchWizard({
9607
10142
  explicit: { providerId: launch.launchProvider, modelId: launch.launchModel },
@@ -9696,9 +10231,17 @@ Error: ${launchPlan.error}
9696
10231
  const pickedProvider = await pickCodexProvider(compatible, prefs, favoritesActive);
9697
10232
  if (!pickedProvider) return 0;
9698
10233
  if (pickedProvider === "__favorites__") {
9699
- const fav = favorites[0];
9700
- activeProvider = compatible.find((p19) => p19.id === fav.providerId);
9701
- 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;
9702
10245
  p13.log.step(`Loaded Favorites Catalog. Starting model: ${selectedModel.name || selectedModel.id} (${activeProvider.name})`);
9703
10246
  } else {
9704
10247
  activeProvider = pickedProvider;
@@ -9770,7 +10313,11 @@ Error: ${launchPlan.error}
9770
10313
  npm: route.npm,
9771
10314
  apiKey: route.apiKey,
9772
10315
  baseURL: route.baseURL,
9773
- upstreamModelId: route.upstreamModelId
10316
+ upstreamModelId: route.upstreamModelId,
10317
+ providerId: route.providerId,
10318
+ supportedParameters: route.supportedParameters,
10319
+ reasoning: route.reasoning,
10320
+ interleavedReasoningField: route.interleavedReasoningField
9774
10321
  }], { debug: trace });
9775
10322
  proxyPort = proxyHandle.port;
9776
10323
  }
@@ -9928,7 +10475,13 @@ function mergeAppConfig(existing, spec) {
9928
10475
  };
9929
10476
  const existingEffort = typeof out.model_reasoning_effort === "string" ? out.model_reasoning_effort : void 0;
9930
10477
  if (existingEffort !== void 0) {
9931
- 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
+ });
9932
10485
  if (caps.levels.length === 0 || !caps.levels.includes(existingEffort)) {
9933
10486
  if (caps.levels.length > 0 && caps.defaultLevel) {
9934
10487
  out.model_reasoning_effort = caps.defaultLevel;
@@ -10118,7 +10671,7 @@ function removeAppCatalogs(env = process.env) {
10118
10671
  }
10119
10672
  function restoreCodexAppOverlay(env = process.env) {
10120
10673
  const lock = readAppSessionLock(env);
10121
- if (lock && isConcurrentSession(lock)) {
10674
+ if (lock && lock.pid !== process.pid && isConcurrentSession(lock)) {
10122
10675
  return {
10123
10676
  restored: false,
10124
10677
  liveSession: true,
@@ -10147,7 +10700,7 @@ function recoverInterruptedCodexAppSession(env = process.env) {
10147
10700
  const lock = readAppSessionLock(env);
10148
10701
  const managed = isAppManagedConfig(readCodexConfigText());
10149
10702
  if (!lock && !managed) return { recovered: false };
10150
- if (lock && isConcurrentSession(lock)) {
10703
+ if (lock && lock.pid !== process.pid && isConcurrentSession(lock)) {
10151
10704
  return { recovered: false };
10152
10705
  }
10153
10706
  restoreCodexAppOverlay(env);
@@ -10156,7 +10709,7 @@ function recoverInterruptedCodexAppSession(env = process.env) {
10156
10709
  function checkAppSessionLock(isTty, env = process.env) {
10157
10710
  if (!isTty) return { ok: false, reason: "non_tty" };
10158
10711
  const lock = readAppSessionLock(env);
10159
- if (lock && isConcurrentSession(lock)) {
10712
+ if (lock && lock.pid !== process.pid && isConcurrentSession(lock)) {
10160
10713
  return { ok: false, reason: "concurrent", lock };
10161
10714
  }
10162
10715
  return { ok: true };
@@ -10422,13 +10975,154 @@ ${pc14.bold("Examples:")}
10422
10975
  ${pc14.bold("Favorites:")}
10423
10976
  When you have saved favorites via ${pc14.cyan("relay-ai models")}, the Codex App
10424
10977
  picker will show your starting model + favorites for mid-session switching.
10425
- Zen/Go favorites are skipped \u2014 use ${pc14.cyan("relay-ai claude")} or
10426
- ${pc14.cyan("relay-ai server")} for those.`;
10978
+ Zen/Go favorites are included when an OpenCode API key is available.`;
10427
10979
  }
10428
10980
  function providerForCodexPicker(provider) {
10429
10981
  return { ...provider, models: routableModelsForProvider(provider, "codex-app") };
10430
10982
  }
10431
- 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 = {}) {
10432
11126
  if (args.includes("--help") || args.includes("-h")) {
10433
11127
  console.log(codexAppHelpText());
10434
11128
  return 0;
@@ -10465,6 +11159,9 @@ async function runCodexAppCommand(args) {
10465
11159
  p15.log.warn("Recovered from an interrupted codex-app session (restored Codex config).");
10466
11160
  }
10467
11161
  }
11162
+ if (opts.vertex) {
11163
+ return runCodexAppVertexLaunch(configOnly);
11164
+ }
10468
11165
  const catalogSpinner = p15.spinner();
10469
11166
  catalogSpinner.start("Loading your providers...");
10470
11167
  let catalog;
@@ -10501,9 +11198,14 @@ async function runCodexAppCommand(args) {
10501
11198
  const pickedProvider = await pickCodexProvider(compatible, prefs, favoritesActive);
10502
11199
  if (!pickedProvider) return 0;
10503
11200
  if (pickedProvider === "__favorites__") {
10504
- const fav = favorites[0];
10505
- activeProvider = providerForCodexPicker(compatible.find((p19) => p19.id === fav.providerId));
10506
- 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;
10507
11209
  p15.log.step(`Loaded Favorites Catalog. Starting model: ${selectedModel.name || selectedModel.id} (${activeProvider.name})`);
10508
11210
  } else {
10509
11211
  activeProvider = providerForCodexPicker(pickedProvider);
@@ -11078,6 +11780,8 @@ function claudeAppHelpText() {
11078
11780
 
11079
11781
  ${pc15.bold("Usage:")}
11080
11782
  relay-ai claude-app
11783
+ relay-ai claude-app --trace
11784
+ relay-ai claude-app --restore
11081
11785
 
11082
11786
  ${pc15.bold("Description:")}
11083
11787
  Picks a provider and model from ~/.relay-ai/providers.json, patches Claude Desktop config
@@ -11089,6 +11793,7 @@ ${pc15.bold("Platforms:")}
11089
11793
 
11090
11794
  ${pc15.bold("Cleanup:")}
11091
11795
  Ctrl+C stops the proxy and restores your previous Claude config.
11796
+ After a crash: relay-ai claude-app --restore
11092
11797
  `;
11093
11798
  }
11094
11799
  function providerForClaudePicker(provider) {
@@ -11099,6 +11804,14 @@ async function runClaudeAppCommand(args) {
11099
11804
  console.log(claudeAppHelpText());
11100
11805
  return 0;
11101
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}`);
11102
11815
  try {
11103
11816
  claudeAppSupported();
11104
11817
  } catch (err) {
@@ -11196,7 +11909,8 @@ async function runClaudeAppCommand(args) {
11196
11909
  serverPassword: null,
11197
11910
  catalog: createGatewayModelCatalog(serverModels, { maskGatewayIds: true }),
11198
11911
  backends: BACKENDS,
11199
- gateway: { maskGatewayIds: true }
11912
+ gateway: { maskGatewayIds: true },
11913
+ debugLogPath
11200
11914
  });
11201
11915
  const uuid = writeRelayAiConfig(proxyHandle.port);
11202
11916
  writeSessionLock2({
@@ -11667,7 +12381,7 @@ PROVIDER / MODEL DISCOVERY FOR ALEF CONFIG
11667
12381
  5. relay-ai --ai (includes live state section at bottom of output)
11668
12382
 
11669
12383
  ALEF CHECKLIST
11670
- \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)
11671
12385
  \u25A1 Always pass --provider + --model (or provider__model slug) \u2014 never rely on wizard
11672
12386
  \u25A1 Claude: --output-format stream-json (or json) with -p
11673
12387
  \u25A1 Codex: exec --json (not bare codex exec without --json if parsing stdout)
@@ -11873,6 +12587,7 @@ function parseArgs(args) {
11873
12587
  for (const arg of rest) {
11874
12588
  if (arg === "--help" || arg === "-h") parsed2.showHelp = true;
11875
12589
  else if (arg === "--version" || arg === "-v") parsed2.showVersion = true;
12590
+ else if (arg === "--vertex") parsed2.vertex = true;
11876
12591
  }
11877
12592
  return parsed2;
11878
12593
  }
@@ -11893,6 +12608,10 @@ function parseArgs(args) {
11893
12608
  parsed2.trace = true;
11894
12609
  continue;
11895
12610
  }
12611
+ if (arg === "--vertex") {
12612
+ parsed2.vertex = true;
12613
+ continue;
12614
+ }
11896
12615
  if (arg === "--help" || arg === "-h") {
11897
12616
  parsed2.showHelp = true;
11898
12617
  continue;
@@ -12050,7 +12769,7 @@ ${pc16.bold("Behavior:")}
12050
12769
  ${pc16.bold("Vertex env:")}
12051
12770
  ANTHROPIC_VERTEX_PROJECT_ID or GOOGLE_CLOUD_PROJECT \u2014 your GCP project
12052
12771
  GOOGLE_CLOUD_LOCATION or CLOUD_ML_REGION \u2014 region (default: global)
12053
- 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)
12054
12773
 
12055
12774
  ${pc16.bold("Endpoints:")}
12056
12775
  Anthropic-compatible: ANTHROPIC_BASE_URL=http://127.0.0.1:17645/anthropic
@@ -12336,9 +13055,13 @@ Error: ${launchPlan.error}
12336
13055
  }
12337
13056
  const providerChoice = chosen;
12338
13057
  if (providerChoice === "__favorites__") {
12339
- const fav = favorites[0];
12340
- activeProvider = allProviders.find((p19) => p19.id === fav.providerId);
12341
- 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;
12342
13065
  p18.log.step(`Loaded Favorites Catalog. Starting model: ${selectedModel.name || selectedModel.id} (${activeProvider.name})`);
12343
13066
  } else {
12344
13067
  activeProvider = allProviders.find((lp) => lp.id === providerChoice);
@@ -12434,7 +13157,11 @@ Error: ${launchPlan.error}
12434
13157
  {
12435
13158
  npm: selectedModel.npm,
12436
13159
  baseURL: selectedModel.apiBaseUrl,
12437
- upstreamModelId: selectedModel.upstreamModelId
13160
+ upstreamModelId: selectedModel.upstreamModelId,
13161
+ providerId: activeProvider.id,
13162
+ supportedParameters: selectedModel.supportedParameters,
13163
+ reasoning: selectedModel.reasoning,
13164
+ interleavedReasoningField: selectedModel.interleavedReasoningField
12438
13165
  }
12439
13166
  );
12440
13167
  if (!isAgentStdoutMode()) {
@@ -12534,7 +13261,7 @@ Error: ${parsed.error}
12534
13261
  console.log(VERSION);
12535
13262
  return 0;
12536
13263
  }
12537
- return runCodexAppCommand(parsed.claudeArgs);
13264
+ return runCodexAppCommand(parsed.claudeArgs, { vertex: parsed.vertex });
12538
13265
  }
12539
13266
  if (parsed.command === "claude-app") {
12540
13267
  if (parsed.showVersion) {
@@ -12554,7 +13281,8 @@ Error: ${parsed.error}
12554
13281
  }
12555
13282
  return runCodexCommand(parsed.claudeArgs, parsed.trace, {
12556
13283
  launchProvider: parsed.launchProvider,
12557
- launchModel: parsed.launchModel
13284
+ launchModel: parsed.launchModel,
13285
+ vertex: parsed.vertex
12558
13286
  });
12559
13287
  }
12560
13288
  if (parsed.showVersion) {