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