@jacobbd/relay-ai 0.7.1 → 0.7.3
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/README.md +3 -3
- package/dist/{chunk-6CBNKM55.js → chunk-5LPUIWNJ.js} +123 -13
- package/dist/chunk-5LPUIWNJ.js.map +1 -0
- package/dist/cli.js +248 -119
- package/dist/cli.js.map +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/ui/public/app.js +139 -7
- package/dist/ui/public/provider-model-browser.js +27 -8
- package/dist/ui/public/style.css +122 -5
- package/dist/{ui-command-S3TQKB3D.js → ui-command-DMPYSK7W.js} +74 -5
- package/dist/ui-command-DMPYSK7W.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-6CBNKM55.js.map +0 -1
- package/dist/ui-command-S3TQKB3D.js.map +0 -1
package/README.md
CHANGED
|
@@ -216,7 +216,7 @@ To skip that question, use `--http-proxy`. A selected model does not need to be
|
|
|
216
216
|
relay-ai claude --http-proxy --provider moonshot --model kimi-k3
|
|
217
217
|
```
|
|
218
218
|
|
|
219
|
-
Using `--http-proxy` without a provider/model keeps native Claude as the starting model and adds any compatible saved favorites
|
|
219
|
+
Using `--http-proxy` without a provider/model keeps native Claude as the starting model and adds any compatible saved favorites into Claude Code's normal `/model` picker (alongside your Anthropic subscription models). The temporary local connection is password-protected, uses a per-session certificate, and closes automatically when Claude Code exits. Existing non-local network or corporate proxy settings are rejected with a clear error because proxy chaining is not supported yet.
|
|
220
220
|
|
|
221
221
|
> **Compatibility:** This mixed Anthropic + Relay mode currently supports Claude Code connected directly to Anthropic. It does not preserve Google Vertex AI configuration. Vertex users should continue using their normal Claude Code launch or Relay-only mode.
|
|
222
222
|
|
|
@@ -232,7 +232,7 @@ Add up to 20 favorites from Zen, Go, or any OpenCode-configured provider. When y
|
|
|
232
232
|
|
|
233
233
|
No favorites? Launch works like before: single model, no switch menu. `--dry-run` ignores saved favorites so you can preview a single-model launch.
|
|
234
234
|
|
|
235
|
-
That built-in favorites catalog applies to **Relay-only mode** (choose **No** when asked whether to keep your normal Claude models). In mixed Anthropic + Relay mode
|
|
235
|
+
That built-in favorites catalog applies to **Relay-only mode** (choose **No** when asked whether to keep your normal Claude models). In mixed Anthropic + Relay mode (`--http-proxy` / choose **Yes**), compatible favorites also show up in Claude Code's normal `/model` picker next to your Anthropic models.
|
|
236
236
|
|
|
237
237
|
#### `relay-ai claude` options
|
|
238
238
|
|
|
@@ -556,7 +556,7 @@ When you launch, relay-ai builds a clean child environment:
|
|
|
556
556
|
|
|
557
557
|
When Claude Code exits (normal exit, Ctrl+C, terminal close), your shell is unchanged. No cleanup step. No restore needed.
|
|
558
558
|
|
|
559
|
-
In `--http-proxy` mode, relay-ai does not replace your Anthropic credential. It removes stale third-party endpoint/cloud overrides from the child process, sets a password-protected loopback proxy plus a temporary CA bundle, and forwards native Anthropic requests—including their original auth and body—unchanged. Only the selected Relay model and compatible favorites are allowed to use registry provider credentials.
|
|
559
|
+
In `--http-proxy` mode, relay-ai does not replace your Anthropic credential. It removes stale third-party endpoint/cloud overrides from the child process, sets a password-protected loopback proxy plus a temporary CA bundle, points Claude Code at a local sentinel Anthropic host (still forwarded to real Anthropic), seeds the gateway models cache so favorites appear in `/model`, and forwards native Anthropic requests—including their original auth and body—unchanged (restoring Host + billing attribution headers on the way out). Only the selected Relay model and compatible favorites are allowed to use registry provider credentials.
|
|
560
560
|
|
|
561
561
|
**Caveat: Claude Code persists the model.** relay-ai doesn't edit `~/.claude/settings.json`, but Claude Code saves the model you launched with (via `--model` and `ANTHROPIC_MODEL`). A later bare `claude` launch may still show that model, e.g. `anthropic-opencode-go__deepseek-v4-flash` from a prior relay-ai session. To get back to a first-party default, run `claude --model sonnet` (or your preferred Claude model), or remove the `"model"` key from `~/.claude/settings.json`. If you used the favorites switch menu, Claude Code may also cache the gateway catalog at `~/.claude/cache/gateway-models.json`. Delete that file if `/model` shows stale entries from a dead proxy.
|
|
562
562
|
|
|
@@ -11,7 +11,7 @@ import { join } from "path";
|
|
|
11
11
|
// package.json
|
|
12
12
|
var package_default = {
|
|
13
13
|
name: "@jacobbd/relay-ai",
|
|
14
|
-
version: "0.7.
|
|
14
|
+
version: "0.7.3",
|
|
15
15
|
publishConfig: {
|
|
16
16
|
access: "public"
|
|
17
17
|
},
|
|
@@ -1900,6 +1900,24 @@ function setServerListenMode(listenMode) {
|
|
|
1900
1900
|
};
|
|
1901
1901
|
writeConfig(config);
|
|
1902
1902
|
}
|
|
1903
|
+
function getServerAutostart() {
|
|
1904
|
+
return readConfig().server?.autostart ?? false;
|
|
1905
|
+
}
|
|
1906
|
+
function setServerAutostart(autostart) {
|
|
1907
|
+
const config = readConfig();
|
|
1908
|
+
config.server = {
|
|
1909
|
+
...config.server ?? {},
|
|
1910
|
+
autostart
|
|
1911
|
+
};
|
|
1912
|
+
writeConfig(config);
|
|
1913
|
+
}
|
|
1914
|
+
function resolveServerAutostart(env = process.env) {
|
|
1915
|
+
const envVal = env["RELAY_AI_SERVER_AUTOSTART"]?.trim().toLowerCase();
|
|
1916
|
+
if (envVal !== void 0 && envVal !== "") {
|
|
1917
|
+
return ["1", "true", "yes", "on"].includes(envVal);
|
|
1918
|
+
}
|
|
1919
|
+
return getServerAutostart();
|
|
1920
|
+
}
|
|
1903
1921
|
|
|
1904
1922
|
// src/launch.ts
|
|
1905
1923
|
import { execSync, spawn } from "child_process";
|
|
@@ -4429,6 +4447,52 @@ function formatOpenAIModels(models) {
|
|
|
4429
4447
|
};
|
|
4430
4448
|
}
|
|
4431
4449
|
|
|
4450
|
+
// src/anthropic-endpoints.ts
|
|
4451
|
+
var MESSAGE_PATH = "/v1/messages";
|
|
4452
|
+
var COUNT_TOKENS_PATH = "/v1/messages/count_tokens";
|
|
4453
|
+
var MODELS_PATH = "/v1/models";
|
|
4454
|
+
function anthropicModelsEndpoint(url) {
|
|
4455
|
+
if (!url) return null;
|
|
4456
|
+
try {
|
|
4457
|
+
const pathname = new URL(url, "http://relay.local").pathname;
|
|
4458
|
+
if (pathname === MODELS_PATH || pathname === `${MODELS_PATH}/`) return "list";
|
|
4459
|
+
if (pathname.startsWith(`${MODELS_PATH}/`)) {
|
|
4460
|
+
const id = decodeURIComponent(pathname.slice(MODELS_PATH.length + 1));
|
|
4461
|
+
if (id) return { id };
|
|
4462
|
+
}
|
|
4463
|
+
} catch {
|
|
4464
|
+
}
|
|
4465
|
+
return null;
|
|
4466
|
+
}
|
|
4467
|
+
function anthropicMessagesEndpoint(url) {
|
|
4468
|
+
if (!url) return null;
|
|
4469
|
+
try {
|
|
4470
|
+
const pathname = new URL(url, "http://relay.local").pathname;
|
|
4471
|
+
if (pathname === MESSAGE_PATH) return "messages";
|
|
4472
|
+
if (pathname === COUNT_TOKENS_PATH) return "count_tokens";
|
|
4473
|
+
} catch {
|
|
4474
|
+
}
|
|
4475
|
+
return null;
|
|
4476
|
+
}
|
|
4477
|
+
var NON_CONTEXT_FIELDS = /* @__PURE__ */ new Set([
|
|
4478
|
+
"model",
|
|
4479
|
+
"stream",
|
|
4480
|
+
"max_tokens",
|
|
4481
|
+
"temperature",
|
|
4482
|
+
"top_p",
|
|
4483
|
+
"top_k",
|
|
4484
|
+
"stop_sequences",
|
|
4485
|
+
"metadata"
|
|
4486
|
+
]);
|
|
4487
|
+
function estimateAnthropicInputTokens(body) {
|
|
4488
|
+
const contextBody = Object.fromEntries(
|
|
4489
|
+
Object.entries(body).filter(([key]) => !NON_CONTEXT_FIELDS.has(key))
|
|
4490
|
+
);
|
|
4491
|
+
const serialized = JSON.stringify(contextBody);
|
|
4492
|
+
if (!serialized || serialized === "{}") return 0;
|
|
4493
|
+
return Math.max(1, Math.ceil(Buffer.byteLength(serialized, "utf8") / 4));
|
|
4494
|
+
}
|
|
4495
|
+
|
|
4432
4496
|
// src/upstream-forward.ts
|
|
4433
4497
|
import { Readable } from "stream";
|
|
4434
4498
|
|
|
@@ -4646,7 +4710,9 @@ function encodeToolUseId(rawId, thoughtSignature, inline = true) {
|
|
|
4646
4710
|
return `${rawId}${TOOL_USE_SIG_SEP}${encoded}`;
|
|
4647
4711
|
}
|
|
4648
4712
|
function serializeToolResultContent(content) {
|
|
4649
|
-
|
|
4713
|
+
if (typeof content === "string") return content;
|
|
4714
|
+
if (content === void 0) return "";
|
|
4715
|
+
return JSON.stringify(content);
|
|
4650
4716
|
}
|
|
4651
4717
|
|
|
4652
4718
|
// src/antigravity/request-adapter.ts
|
|
@@ -5432,6 +5498,8 @@ function sanitizeMessage(message) {
|
|
|
5432
5498
|
}
|
|
5433
5499
|
|
|
5434
5500
|
// src/sdk-adapter.ts
|
|
5501
|
+
var NEEDS_TRAILING_TOOL_NUDGE = /* @__PURE__ */ new Set(["@ai-sdk/alibaba"]);
|
|
5502
|
+
var TRAILING_TOOL_NUDGE_TEXT = "Continue.";
|
|
5435
5503
|
function anthropicEffortFromRequest(body) {
|
|
5436
5504
|
const effort = body.output_config?.effort;
|
|
5437
5505
|
if (typeof effort === "string" && effort.trim()) return effort.trim();
|
|
@@ -5492,7 +5560,7 @@ function thinkingToSdkPart(block, npm) {
|
|
|
5492
5560
|
}
|
|
5493
5561
|
return part;
|
|
5494
5562
|
}
|
|
5495
|
-
function translateMessages(messages, npm) {
|
|
5563
|
+
function translateMessages(messages, npm, onDebug) {
|
|
5496
5564
|
const isGoogle = npm === "@ai-sdk/google";
|
|
5497
5565
|
const out = [];
|
|
5498
5566
|
for (const msg of messages) {
|
|
@@ -5507,6 +5575,10 @@ function translateMessages(messages, npm) {
|
|
|
5507
5575
|
if (p8) parts.push(p8);
|
|
5508
5576
|
}
|
|
5509
5577
|
}
|
|
5578
|
+
if (blocks.length && !toolResults.length && !parts.length) {
|
|
5579
|
+
const types = blocks.map((b) => b.type).join(", ");
|
|
5580
|
+
onDebug?.(`sdk: dropped user turn with unrecognized block types: [${types}]`);
|
|
5581
|
+
}
|
|
5510
5582
|
if (toolResults.length) {
|
|
5511
5583
|
out.push({
|
|
5512
5584
|
role: "tool",
|
|
@@ -5542,6 +5614,10 @@ function translateMessages(messages, npm) {
|
|
|
5542
5614
|
if (parts.length) out.push({ role: "assistant", content: parts });
|
|
5543
5615
|
}
|
|
5544
5616
|
}
|
|
5617
|
+
if (NEEDS_TRAILING_TOOL_NUDGE.has(npm) && out[out.length - 1]?.role === "tool") {
|
|
5618
|
+
out.push({ role: "user", content: [{ type: "text", text: TRAILING_TOOL_NUDGE_TEXT }] });
|
|
5619
|
+
onDebug?.("sdk: appended continuation nudge (request ended on tool result)");
|
|
5620
|
+
}
|
|
5545
5621
|
return out;
|
|
5546
5622
|
}
|
|
5547
5623
|
function stripNullInputs(input) {
|
|
@@ -5592,7 +5668,7 @@ function translateRequest2(body, npm, options) {
|
|
|
5592
5668
|
}
|
|
5593
5669
|
return {
|
|
5594
5670
|
system: options?.openAiOAuth ? void 0 : systemText,
|
|
5595
|
-
messages: translateMessages(messages, npm),
|
|
5671
|
+
messages: translateMessages(messages, npm, options?.onDebug),
|
|
5596
5672
|
tools: translateTools3(upstreamTools.length ? upstreamTools : void 0),
|
|
5597
5673
|
toolChoice: translateToolChoice(body.tool_choice),
|
|
5598
5674
|
maxOutputTokens: options?.openAiOAuth ? void 0 : body.max_tokens,
|
|
@@ -5600,7 +5676,7 @@ function translateRequest2(body, npm, options) {
|
|
|
5600
5676
|
providerOptions
|
|
5601
5677
|
};
|
|
5602
5678
|
}
|
|
5603
|
-
async function writeAnthropicStream(fullStream, modelId, write, log7) {
|
|
5679
|
+
async function writeAnthropicStream(fullStream, modelId, write, log7, estimatedInputTokens = 0) {
|
|
5604
5680
|
const messageId = "msg_" + Date.now();
|
|
5605
5681
|
let blockIndex = -1;
|
|
5606
5682
|
let started = false;
|
|
@@ -5608,7 +5684,7 @@ async function writeAnthropicStream(fullStream, modelId, write, log7) {
|
|
|
5608
5684
|
let pendingThinkingSig;
|
|
5609
5685
|
const idToBlock = /* @__PURE__ */ new Map();
|
|
5610
5686
|
let finishReason = "end_turn";
|
|
5611
|
-
let usage = { input_tokens:
|
|
5687
|
+
let usage = { input_tokens: estimatedInputTokens, output_tokens: 0 };
|
|
5612
5688
|
const emit = (event, data) => write(sseChunk(event, data));
|
|
5613
5689
|
const ensureStart = () => {
|
|
5614
5690
|
if (started) return;
|
|
@@ -5622,7 +5698,7 @@ async function writeAnthropicStream(fullStream, modelId, write, log7) {
|
|
|
5622
5698
|
model: modelId,
|
|
5623
5699
|
stop_reason: null,
|
|
5624
5700
|
stop_sequence: null,
|
|
5625
|
-
usage: { input_tokens:
|
|
5701
|
+
usage: { input_tokens: estimatedInputTokens, output_tokens: 0 }
|
|
5626
5702
|
}
|
|
5627
5703
|
});
|
|
5628
5704
|
started = true;
|
|
@@ -5721,7 +5797,7 @@ async function writeAnthropicStream(fullStream, modelId, write, log7) {
|
|
|
5721
5797
|
case "finish":
|
|
5722
5798
|
if (part.totalUsage) {
|
|
5723
5799
|
usage = {
|
|
5724
|
-
input_tokens: part.totalUsage.inputTokens ??
|
|
5800
|
+
input_tokens: part.totalUsage.inputTokens ?? estimatedInputTokens,
|
|
5725
5801
|
output_tokens: part.totalUsage.outputTokens ?? 0
|
|
5726
5802
|
};
|
|
5727
5803
|
}
|
|
@@ -5747,7 +5823,7 @@ async function writeAnthropicStream(fullStream, modelId, write, log7) {
|
|
|
5747
5823
|
emit("message_delta", { type: "message_delta", delta: { stop_reason: finishReason, stop_sequence: null }, usage });
|
|
5748
5824
|
emit("message_stop", { type: "message_stop" });
|
|
5749
5825
|
}
|
|
5750
|
-
async function streamAnthropicResponse(model, params, modelId, write, log7) {
|
|
5826
|
+
async function streamAnthropicResponse(model, params, modelId, write, log7, estimatedInputTokens = 0) {
|
|
5751
5827
|
const result = streamText({ model, ...params, onError: () => {
|
|
5752
5828
|
} });
|
|
5753
5829
|
Promise.resolve(result.text).catch(() => {
|
|
@@ -5760,7 +5836,13 @@ async function streamAnthropicResponse(model, params, modelId, write, log7) {
|
|
|
5760
5836
|
});
|
|
5761
5837
|
Promise.resolve(result.usage).catch(() => {
|
|
5762
5838
|
});
|
|
5763
|
-
await writeAnthropicStream(
|
|
5839
|
+
await writeAnthropicStream(
|
|
5840
|
+
result.fullStream,
|
|
5841
|
+
modelId,
|
|
5842
|
+
write,
|
|
5843
|
+
log7,
|
|
5844
|
+
estimatedInputTokens
|
|
5845
|
+
);
|
|
5764
5846
|
}
|
|
5765
5847
|
async function generateAnthropicResponse(model, params, modelId, options) {
|
|
5766
5848
|
let text4;
|
|
@@ -5965,6 +6047,7 @@ function startProxyCatalog(routes, defaultAliasId, debug = false) {
|
|
|
5965
6047
|
const params = translateRequest2(anthropicBody, route.npm, {
|
|
5966
6048
|
openAiOAuth,
|
|
5967
6049
|
maxTools: maxToolsForNpm(route.npm),
|
|
6050
|
+
onDebug: (msg) => plog(() => msg),
|
|
5968
6051
|
reasoningMetadata: {
|
|
5969
6052
|
providerId: route.providerId,
|
|
5970
6053
|
apiBaseUrl: route.baseURL,
|
|
@@ -5998,7 +6081,14 @@ function startProxyCatalog(routes, defaultAliasId, debug = false) {
|
|
|
5998
6081
|
"Cache-Control": "no-cache",
|
|
5999
6082
|
"Connection": "keep-alive"
|
|
6000
6083
|
});
|
|
6001
|
-
await streamAnthropicResponse(
|
|
6084
|
+
await streamAnthropicResponse(
|
|
6085
|
+
model,
|
|
6086
|
+
params,
|
|
6087
|
+
originalModel,
|
|
6088
|
+
(c) => res.write(c),
|
|
6089
|
+
plog,
|
|
6090
|
+
estimateAnthropicInputTokens(anthropicBody)
|
|
6091
|
+
);
|
|
6002
6092
|
res.end();
|
|
6003
6093
|
} else {
|
|
6004
6094
|
const anthropicResponse = await generateAnthropicResponse(
|
|
@@ -9870,6 +9960,7 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
|
|
|
9870
9960
|
const params = translateRequest2(body, model.npm, {
|
|
9871
9961
|
defaultEffort: anthropicEffortFromRequest(body) ? void 0 : model.defaultEffort,
|
|
9872
9962
|
openAiOAuth: model.npm === "@ai-sdk/openai" && model.authType === "oauth",
|
|
9963
|
+
onDebug: plog,
|
|
9873
9964
|
reasoningMetadata: {
|
|
9874
9965
|
providerId: model.providerId,
|
|
9875
9966
|
apiBaseUrl: model.apiBaseUrl,
|
|
@@ -9890,7 +9981,14 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
|
|
|
9890
9981
|
"Cache-Control": "no-cache",
|
|
9891
9982
|
"Connection": "keep-alive"
|
|
9892
9983
|
});
|
|
9893
|
-
await streamAnthropicResponse(
|
|
9984
|
+
await streamAnthropicResponse(
|
|
9985
|
+
languageModel,
|
|
9986
|
+
params,
|
|
9987
|
+
responseModelId,
|
|
9988
|
+
(chunk) => res.write(chunk),
|
|
9989
|
+
void 0,
|
|
9990
|
+
estimateAnthropicInputTokens(body)
|
|
9991
|
+
);
|
|
9894
9992
|
res.end();
|
|
9895
9993
|
} else {
|
|
9896
9994
|
const anthropicResponse = await generateAnthropicResponse(languageModel, params, responseModelId);
|
|
@@ -10904,12 +11002,17 @@ function buildHttpProxyRoutes(providers, favorites, selected, max = MAX_MODEL_CA
|
|
|
10904
11002
|
unavailable.push(item);
|
|
10905
11003
|
continue;
|
|
10906
11004
|
}
|
|
11005
|
+
const gatewayAliasId2 = claudeCodeClientModelId(
|
|
11006
|
+
aliasModelId(model.id, provider.id),
|
|
11007
|
+
model.contextWindow
|
|
11008
|
+
);
|
|
10907
11009
|
routes.push({
|
|
10908
11010
|
...route,
|
|
10909
11011
|
aliasId: claudeCodeClientModelId(
|
|
10910
11012
|
httpProxyModelId(provider.id, model.id),
|
|
10911
11013
|
model.contextWindow
|
|
10912
11014
|
),
|
|
11015
|
+
gatewayAliasId: gatewayAliasId2,
|
|
10913
11016
|
displayName: `${model.name || model.id} (${provider.name})`
|
|
10914
11017
|
});
|
|
10915
11018
|
}
|
|
@@ -11813,6 +11916,8 @@ export {
|
|
|
11813
11916
|
setServerFreeModelsOnly,
|
|
11814
11917
|
getServerListenMode,
|
|
11815
11918
|
setServerListenMode,
|
|
11919
|
+
setServerAutostart,
|
|
11920
|
+
resolveServerAutostart,
|
|
11816
11921
|
findBinaryOnPath,
|
|
11817
11922
|
findClaudeBinary,
|
|
11818
11923
|
launchClaude,
|
|
@@ -11880,10 +11985,15 @@ export {
|
|
|
11880
11985
|
readBody,
|
|
11881
11986
|
extractApiKey,
|
|
11882
11987
|
sendJson,
|
|
11988
|
+
formatAnthropicModelEntry,
|
|
11989
|
+
formatAnthropicModelList,
|
|
11883
11990
|
gatewayProviderLabel,
|
|
11884
11991
|
openAiIdCollisions,
|
|
11885
11992
|
createGatewayModelCatalog,
|
|
11886
11993
|
buildDedupedModelRows,
|
|
11994
|
+
anthropicModelsEndpoint,
|
|
11995
|
+
anthropicMessagesEndpoint,
|
|
11996
|
+
estimateAnthropicInputTokens,
|
|
11887
11997
|
grabRoundTripSignature,
|
|
11888
11998
|
silenceSdkWarnings,
|
|
11889
11999
|
parseToolArguments,
|
|
@@ -11959,4 +12069,4 @@ export {
|
|
|
11959
12069
|
supportsClaudeTransparentMode,
|
|
11960
12070
|
buildHttpProxyRoutes
|
|
11961
12071
|
};
|
|
11962
|
-
//# sourceMappingURL=chunk-
|
|
12072
|
+
//# sourceMappingURL=chunk-5LPUIWNJ.js.map
|