@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/AGENTS.md +2 -2
- package/CHANGELOG.md +8 -80
- package/README.md +3 -3
- package/dist/cli.js +1023 -295
- package/package.json +1 -1
- /package/{vertex-models.example.json → assets/vertex-models.example.json} +0 -0
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.
|
|
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
|
-
|
|
363
|
-
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
|
429
|
-
if (isGemini3Model(
|
|
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
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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 ??
|
|
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(
|
|
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,
|
|
3950
|
-
const result = await fetchTemplateModels(template, key,
|
|
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
|
-
|
|
4068
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
4876
|
+
const text5 = await upstreamRes.text();
|
|
4672
4877
|
try {
|
|
4673
|
-
|
|
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(
|
|
4886
|
+
"Content-Length": Buffer.byteLength(text5).toString()
|
|
4683
4887
|
});
|
|
4684
|
-
res.end(
|
|
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 = "
|
|
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
|
-
|
|
4725
|
-
if (sep
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
6049
|
-
|
|
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
|
|
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
|
-
|
|
6245
|
-
|
|
6246
|
-
|
|
6247
|
-
|
|
6248
|
-
|
|
6249
|
-
|
|
6250
|
-
|
|
6251
|
-
|
|
6252
|
-
|
|
6253
|
-
|
|
6254
|
-
|
|
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) =>
|
|
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(
|
|
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,
|
|
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
|
-
|
|
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 (!
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
8101
|
+
const query = String(searchInput);
|
|
8102
|
+
const matched = filterTemplates(templates, query);
|
|
7691
8103
|
if (matched.length === 0) {
|
|
7692
|
-
|
|
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
|
-
{
|
|
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 =
|
|
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
|
|
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
|
-
|
|
8217
|
-
|
|
8218
|
-
let
|
|
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
|
-
|
|
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
|
-
|
|
8310
|
-
if (
|
|
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
|
-
|
|
8321
|
-
|
|
8322
|
-
|
|
8323
|
-
|
|
8324
|
-
|
|
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
|
-
|
|
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:
|
|
8412
|
-
output_index:
|
|
8413
|
-
arguments:
|
|
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:
|
|
8418
|
-
call_id:
|
|
8419
|
-
name:
|
|
8420
|
-
arguments:
|
|
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:
|
|
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:
|
|
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)
|
|
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 =
|
|
9381
|
+
return effort ? `model_reasoning_effort = ${tomlString(effort)}
|
|
8920
9382
|
` : "";
|
|
8921
9383
|
}
|
|
8922
9384
|
function profileSandboxLine() {
|
|
8923
|
-
return `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 =
|
|
8935
|
-
model_provider =
|
|
8936
|
-
model_catalog_json =
|
|
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 =
|
|
8940
|
-
base_url =
|
|
8941
|
-
env_key =
|
|
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 =
|
|
9412
|
+
${profileSandboxLine()}model = ${tomlString(model)}
|
|
8948
9413
|
model_provider = "relay-ai-proxy"
|
|
8949
|
-
model_catalog_json =
|
|
9414
|
+
model_catalog_json = ${tomlString(catalogPath)}
|
|
8950
9415
|
${reasoning}
|
|
8951
9416
|
[model_providers.relay-ai-proxy]
|
|
8952
9417
|
name = "relay-ai"
|
|
8953
|
-
base_url =
|
|
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
|
|
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
|
-
|
|
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
|
|
9700
|
-
|
|
9701
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
10505
|
-
|
|
10506
|
-
|
|
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
|
|
12340
|
-
|
|
12341
|
-
|
|
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) {
|