@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 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. Relay models cannot be inserted into Claude Code's built-in model picker, so the terminal prints the exact `/model relay:<provider>:<model>` commands you can use to switch. 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.
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, compatible favorites are available through the printed `/model relay:<provider>:<model>` commands instead; Claude Code cannot add them to its built-in picker.
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.1",
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
- return typeof content === "string" ? content : JSON.stringify(content);
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: 0, output_tokens: 0 };
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: 0, output_tokens: 0 }
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 ?? 0,
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(result.fullStream, modelId, write, log7);
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(model, params, originalModel, (c) => res.write(c), plog);
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(languageModel, params, responseModelId, (chunk) => res.write(chunk));
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-6CBNKM55.js.map
12072
+ //# sourceMappingURL=chunk-5LPUIWNJ.js.map