@jacobbd/relay-ai 0.12.3 → 0.12.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -78,6 +78,7 @@ import {
78
78
  silenceSdkWarnings,
79
79
  slugifyProviderId,
80
80
  splitToolUseId,
81
+ sseChunk,
81
82
  streamAnthropicResponse,
82
83
  stripOneMContextSuffix,
83
84
  supportsNativeOAuth,
@@ -85,7 +86,7 @@ import {
85
86
  translateRequest,
86
87
  upstreamHttpStatus,
87
88
  validateCustomEndpointUrl
88
- } from "./chunk-C7TTIGMR.js";
89
+ } from "./chunk-2LUDFIZX.js";
89
90
 
90
91
  // src/registry/google-model-id.ts
91
92
  var GOOGLE_MODEL_PREFIX = "models/";
@@ -1901,7 +1902,7 @@ function estimateAnthropicInputTokens(body) {
1901
1902
  }
1902
1903
 
1903
1904
  // src/upstream-forward.ts
1904
- import { Readable } from "stream";
1905
+ import { once } from "events";
1905
1906
 
1906
1907
  // src/server/auth.ts
1907
1908
  function sanitizeCredential(value) {
@@ -1959,7 +1960,7 @@ async function fetchWithOAuthRetry(apiKey, request, refreshToken) {
1959
1960
  response = await request(refreshed);
1960
1961
  return { response, apiKey: refreshed, refreshed: true };
1961
1962
  }
1962
- async function relayAnthropicMessages(res, messagesUrl, body, apiKey, clientWantsStream, inboundBeta, authType, log7, claudeCodeSessionId, extraHeaders, refreshToken, onTokenRefreshed) {
1963
+ async function relayAnthropicMessages(res, messagesUrl, body, apiKey, clientWantsStream, inboundBeta, authType, log7, claudeCodeSessionId, extraHeaders, refreshToken, onTokenRefreshed, retryEmptyStream = false) {
1963
1964
  const doFetch = (key) => fetch(messagesUrl, {
1964
1965
  method: "POST",
1965
1966
  headers: anthropicUpstreamHeaders(key, clientWantsStream, inboundBeta, authType, claudeCodeSessionId, extraHeaders),
@@ -1981,12 +1982,46 @@ async function relayAnthropicMessages(res, messagesUrl, body, apiKey, clientWant
1981
1982
  return;
1982
1983
  }
1983
1984
  if (clientWantsStream && upstreamRes.body) {
1985
+ const reader = upstreamRes.body.getReader();
1986
+ const first = await reader.read().catch(() => ({ done: true, value: void 0 }));
1987
+ if (first.done) {
1988
+ reader.releaseLock();
1989
+ if (retryEmptyStream) {
1990
+ log7?.("anthropic upstream returned an empty stream; retrying without streaming");
1991
+ await replayWithoutStreaming(
1992
+ res,
1993
+ messagesUrl,
1994
+ body,
1995
+ apiKey,
1996
+ inboundBeta,
1997
+ authType,
1998
+ claudeCodeSessionId,
1999
+ extraHeaders,
2000
+ log7
2001
+ );
2002
+ return;
2003
+ }
2004
+ }
1984
2005
  res.writeHead(200, {
1985
2006
  "Content-Type": "text/event-stream",
1986
2007
  "Cache-Control": "no-cache",
1987
2008
  "Connection": "keep-alive"
1988
2009
  });
1989
- Readable.fromWeb(upstreamRes.body).on("error", () => res.destroy()).pipe(res);
2010
+ if (first.done) {
2011
+ res.end();
2012
+ return;
2013
+ }
2014
+ res.write(Buffer.from(first.value));
2015
+ try {
2016
+ while (true) {
2017
+ const next = await reader.read();
2018
+ if (next.done) break;
2019
+ if (!res.write(Buffer.from(next.value))) await once(res, "drain");
2020
+ }
2021
+ res.end();
2022
+ } catch {
2023
+ res.destroy();
2024
+ }
1990
2025
  return;
1991
2026
  }
1992
2027
  if (!upstreamRes.body) {
@@ -2008,6 +2043,85 @@ async function relayAnthropicMessages(res, messagesUrl, body, apiKey, clientWant
2008
2043
  });
2009
2044
  res.end(text4);
2010
2045
  }
2046
+ async function replayWithoutStreaming(res, messagesUrl, body, apiKey, inboundBeta, authType, claudeCodeSessionId, extraHeaders, log7) {
2047
+ let retryRes;
2048
+ try {
2049
+ retryRes = await fetch(messagesUrl, {
2050
+ method: "POST",
2051
+ headers: anthropicUpstreamHeaders(apiKey, false, inboundBeta, authType, claudeCodeSessionId, extraHeaders),
2052
+ body: JSON.stringify({ ...body, stream: false })
2053
+ });
2054
+ } catch (err) {
2055
+ throw new UpstreamUnreachableError(err);
2056
+ }
2057
+ const text4 = await retryRes.text();
2058
+ if (!retryRes.ok) {
2059
+ log7?.(`anthropic upstream ${retryRes.status} on empty-stream retry: ${text4}`);
2060
+ res.writeHead(retryRes.status, { "Content-Type": retryRes.headers.get("content-type") || "application/json" });
2061
+ res.end(text4);
2062
+ return;
2063
+ }
2064
+ let message;
2065
+ try {
2066
+ message = JSON.parse(text4);
2067
+ } catch {
2068
+ res.writeHead(502, { "Content-Type": "application/json" });
2069
+ res.end(JSON.stringify({ type: "error", error: { type: "api_error", message: "Upstream response was not valid JSON" } }));
2070
+ return;
2071
+ }
2072
+ res.writeHead(200, {
2073
+ "Content-Type": "text/event-stream",
2074
+ "Cache-Control": "no-cache",
2075
+ "Connection": "keep-alive"
2076
+ });
2077
+ writeMessageAsSse(res, message);
2078
+ }
2079
+ function writeMessageAsSse(res, message) {
2080
+ const content = Array.isArray(message.content) ? message.content : [];
2081
+ res.write(sseChunk("message_start", {
2082
+ type: "message_start",
2083
+ message: { ...message, content: [], stop_reason: null, stop_sequence: null }
2084
+ }));
2085
+ content.forEach((block, index) => {
2086
+ const type = block.type;
2087
+ const opening = type === "text" ? { type: "text", text: "" } : type === "thinking" ? { type: "thinking", thinking: "", signature: "" } : type === "tool_use" ? { type: "tool_use", id: block.id, name: block.name, input: {} } : block;
2088
+ res.write(sseChunk("content_block_start", { type: "content_block_start", index, content_block: opening }));
2089
+ if (type === "text") {
2090
+ res.write(sseChunk("content_block_delta", {
2091
+ type: "content_block_delta",
2092
+ index,
2093
+ delta: { type: "text_delta", text: block.text ?? "" }
2094
+ }));
2095
+ } else if (type === "thinking") {
2096
+ res.write(sseChunk("content_block_delta", {
2097
+ type: "content_block_delta",
2098
+ index,
2099
+ delta: { type: "thinking_delta", thinking: block.thinking ?? "" }
2100
+ }));
2101
+ if (typeof block.signature === "string") {
2102
+ res.write(sseChunk("content_block_delta", {
2103
+ type: "content_block_delta",
2104
+ index,
2105
+ delta: { type: "signature_delta", signature: block.signature }
2106
+ }));
2107
+ }
2108
+ } else if (type === "tool_use") {
2109
+ res.write(sseChunk("content_block_delta", {
2110
+ type: "content_block_delta",
2111
+ index,
2112
+ delta: { type: "input_json_delta", partial_json: JSON.stringify(block.input ?? {}) }
2113
+ }));
2114
+ }
2115
+ res.write(sseChunk("content_block_stop", { type: "content_block_stop", index }));
2116
+ });
2117
+ res.write(sseChunk("message_delta", {
2118
+ type: "message_delta",
2119
+ delta: { stop_reason: message.stop_reason ?? "end_turn", stop_sequence: message.stop_sequence ?? null },
2120
+ usage: message.usage ?? {}
2121
+ }));
2122
+ res.write(sseChunk("message_stop", { type: "message_stop" }));
2123
+ res.end();
2124
+ }
2011
2125
 
2012
2126
  // src/antigravity/anthropic-to-cloudcode.ts
2013
2127
  import { randomUUID } from "crypto";
@@ -2859,7 +2973,8 @@ function startProxyCatalog(routes, defaultAliasId, debug = false) {
2859
2973
  route.refreshToken,
2860
2974
  (refreshed) => {
2861
2975
  route.apiKey = refreshed;
2862
- }
2976
+ },
2977
+ true
2863
2978
  );
2864
2979
  } catch (err) {
2865
2980
  const message = err instanceof UpstreamUnreachableError ? err.message : String(err);
@@ -3594,8 +3709,7 @@ function readModelsFromModelsDev(backendId, cache = loadModelsDevCache()) {
3594
3709
  const id = entry.id ?? modelKey;
3595
3710
  if (entry.status === "deprecated") continue;
3596
3711
  const isFree = entry.cost !== void 0 && entry.cost.input === 0 && entry.cost.output === 0;
3597
- const rawFormat = classifyModelFormat(id, entry.provider?.npm);
3598
- const modelFormat = backendId === "go" && rawFormat === "anthropic" ? "openai" : rawFormat;
3712
+ const modelFormat = classifyModelFormat(id, entry.provider?.npm);
3599
3713
  result.set(id, {
3600
3714
  id,
3601
3715
  name: entry.name ?? id,
@@ -3630,10 +3744,7 @@ function mergeModels(apiIds, cache, backendId) {
3630
3744
  const uniqueIds = Array.from(new Set(apiIds));
3631
3745
  return uniqueIds.filter((id) => !shouldHideModel({ providerId: backendId, modelId: id, agent: "claude" })).map((id) => {
3632
3746
  const cached = cache?.get(id);
3633
- if (cached) {
3634
- const modelFormat2 = backendId === "go" && cached.modelFormat === "anthropic" ? "openai" : cached.modelFormat;
3635
- return { ...cached, sourceBackend: backendId, modelFormat: modelFormat2 };
3636
- }
3747
+ if (cached) return { ...cached, sourceBackend: backendId };
3637
3748
  const modelFormat = classifyModelFormat(id, void 0);
3638
3749
  return {
3639
3750
  id,
@@ -3919,7 +4030,30 @@ function buildCatalogRoutes(startingRoute, favorites, resolveRoute, max = MAX_MO
3919
4030
 
3920
4031
  // src/registry/materialize.ts
3921
4032
  init_provider_templates();
3922
- function cachedModelToLocal(cached, provider) {
4033
+
4034
+ // src/registry/model-protocol.ts
4035
+ function reconcileCachedModelProtocol(model, provider, metadata = loadModelsDevCache()) {
4036
+ if (model.source === "manual") return model;
4037
+ const isZenGo = provider.id === "zen" || provider.id === "go" || provider.templateId === "zen" || provider.templateId === "go";
4038
+ if (!isZenGo) return model;
4039
+ const metadataNpm = findModelsDevModel(provider.id, model.id, metadata)?.provider?.npm;
4040
+ if (!metadataNpm) return model;
4041
+ const format = classifyModelFormat(model.id, metadataNpm);
4042
+ if (format === "unsupported") return model;
4043
+ const backendId = provider.id === "go" || provider.templateId === "go" ? "go" : "zen";
4044
+ const apiUrl = model.apiUrl ?? provider.api.url ?? BACKENDS[backendId].baseUrl;
4045
+ return {
4046
+ ...model,
4047
+ modelFormat: format,
4048
+ npm: metadataNpm,
4049
+ apiUrl
4050
+ };
4051
+ }
4052
+
4053
+ // src/registry/materialize.ts
4054
+ function cachedModelToLocal(cached, provider, metadata = loadModelsDevCache()) {
4055
+ const reconciled = reconcileCachedModelProtocol(cached, provider, metadata);
4056
+ cached = reconciled;
3923
4057
  const freeStatus = classifyFreeStatus({
3924
4058
  model: cached,
3925
4059
  providerId: provider.id,
@@ -3941,20 +4075,24 @@ function cachedModelToLocal(cached, provider) {
3941
4075
  interleavedReasoningField: cached.interleavedReasoningField
3942
4076
  };
3943
4077
  }
4078
+ const modelsDev = findModelsDevModel(provider.id, cached.id, metadata);
4079
+ const isZenGo = provider.id === "zen" || provider.id === "go" || provider.templateId === "zen" || provider.templateId === "go";
4080
+ const metadataNpm = !cached.source && isZenGo ? modelsDev?.provider?.npm : void 0;
3944
4081
  const npm = cached.npm ?? provider.api.npm ?? "";
3945
- const apiUrl = cached.apiUrl ?? provider.api.url ?? "";
4082
+ const apiUrl = cached.apiUrl ?? provider.api.url ?? (isZenGo ? BACKENDS[provider.id === "go" || provider.templateId === "go" ? "go" : "zen"].baseUrl : "");
3946
4083
  const endpoint = resolveEndpoint(npm, apiUrl);
3947
4084
  if (endpoint === null) return null;
3948
- const modelsDev = findModelsDevModel(provider.id, cached.id);
3949
4085
  const { id, upstreamModelId: upstreamModelId2 } = normalizeGoogleModelId(cached.id, npm);
3950
4086
  const normalizedUpstream = normalizeGoogleModelId(cached.upstreamModelId ?? cached.id, npm).upstreamModelId;
3951
4087
  const family = npm === "@ai-sdk/google" ? id.split(/[-/:]/)[0] ?? id : cached.family ?? "";
4088
+ const classifiedFormat = classifyModelFormat(cached.id, npm);
4089
+ const resolvedFormat = classifiedFormat === "anthropic" ? classifiedFormat : "openai";
3952
4090
  return {
3953
4091
  id,
3954
4092
  name: npm === "@ai-sdk/google" ? normalizeGoogleDisplayName(cached.name, id) : cached.name,
3955
4093
  family,
3956
4094
  brand: npm === "@ai-sdk/google" ? deriveBrand(family) : cached.brand ?? deriveBrand(cached.family ?? ""),
3957
- modelFormat: cached.modelFormat ?? endpoint.format,
4095
+ modelFormat: metadataNpm ? resolvedFormat : cached.modelFormat ?? endpoint.format,
3958
4096
  upstreamModelId: normalizedUpstream,
3959
4097
  baseUrl: endpoint.baseUrl,
3960
4098
  completionsUrl: endpoint.completionsUrl,
@@ -8292,6 +8430,7 @@ function httpProxyModelId(providerId, modelId) {
8292
8430
  return `${HTTP_PROXY_MODEL_PREFIX}${providerId}:${modelId}`;
8293
8431
  }
8294
8432
  function supportsClaudeTransparentMode(model) {
8433
+ if (model.modelFormat === "anthropic") return Boolean(model.baseUrl);
8295
8434
  return model.modelFormat === "openai" && isSdkMigratedNpm(model.npm);
8296
8435
  }
8297
8436
  function buildHttpProxyRoutes(providers, favorites, selected, max = MAX_MODEL_CATALOG) {
@@ -9661,4 +9800,4 @@ export {
9661
9800
  supportsClaudeTransparentMode,
9662
9801
  buildHttpProxyRoutes
9663
9802
  };
9664
- //# sourceMappingURL=chunk-R427D6VA.js.map
9803
+ //# sourceMappingURL=chunk-S7HYE2FI.js.map