llm-relay 0.34.0 → 0.36.1
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 +1 -0
- package/config.example.json +4 -1
- package/dist/anthropic.d.ts +2 -2
- package/dist/anthropic.js +22 -5
- package/dist/anthropic.js.map +1 -1
- package/dist/authEnv.d.ts +7 -0
- package/dist/authEnv.js +9 -0
- package/dist/authEnv.js.map +1 -1
- package/dist/backend.d.ts +6 -0
- package/dist/backend.js +148 -24
- package/dist/backend.js.map +1 -1
- package/dist/candidates.d.ts +3 -1
- package/dist/candidates.js +2 -0
- package/dist/candidates.js.map +1 -1
- package/dist/catalog.js +52 -4
- package/dist/catalog.js.map +1 -1
- package/dist/circuit-breaker.d.ts +5 -0
- package/dist/circuit-breaker.js +37 -4
- package/dist/circuit-breaker.js.map +1 -1
- package/dist/cli.js +105 -77
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +31 -1
- package/dist/config.js +85 -3
- package/dist/config.js.map +1 -1
- package/dist/control-authorization.js +6 -4
- package/dist/control-authorization.js.map +1 -1
- package/dist/dynamic-pools.js +5 -3
- package/dist/dynamic-pools.js.map +1 -1
- package/dist/kernel/contracts.d.ts +2 -0
- package/dist/key-import.d.ts +29 -0
- package/dist/key-import.js +160 -0
- package/dist/key-import.js.map +1 -0
- package/dist/log.d.ts +14 -0
- package/dist/log.js +52 -2
- package/dist/log.js.map +1 -1
- package/dist/onboarding.d.ts +10 -0
- package/dist/onboarding.js +27 -9
- package/dist/onboarding.js.map +1 -1
- package/dist/openai-dialect.d.ts +42 -0
- package/dist/openai-dialect.js +388 -0
- package/dist/openai-dialect.js.map +1 -0
- package/dist/ping/ping.js +1 -0
- package/dist/ping/ping.js.map +1 -1
- package/dist/ping/runtime-telemetry.js +13 -2
- package/dist/ping/runtime-telemetry.js.map +1 -1
- package/dist/process-safety-net.d.ts +46 -0
- package/dist/process-safety-net.js +131 -0
- package/dist/process-safety-net.js.map +1 -0
- package/dist/refusal-interpretation.d.ts +4 -4
- package/dist/refusal-interpretation.js +19 -6
- package/dist/refusal-interpretation.js.map +1 -1
- package/dist/repair.d.ts +7 -0
- package/dist/repair.js +155 -6
- package/dist/repair.js.map +1 -1
- package/dist/request-log.d.ts +2 -2
- package/dist/request-log.js +8 -1
- package/dist/request-log.js.map +1 -1
- package/dist/secret-file-acl.d.ts +25 -0
- package/dist/secret-file-acl.js +37 -0
- package/dist/secret-file-acl.js.map +1 -0
- package/dist/server.d.ts +29 -2
- package/dist/server.js +568 -73
- package/dist/server.js.map +1 -1
- package/dist/session-pin.d.ts +35 -0
- package/dist/session-pin.js +136 -0
- package/dist/session-pin.js.map +1 -0
- package/dist/stream-commit.d.ts +34 -0
- package/dist/stream-commit.js +325 -0
- package/dist/stream-commit.js.map +1 -0
- package/dist/think-tags.d.ts +34 -0
- package/dist/think-tags.js +219 -0
- package/dist/think-tags.js.map +1 -0
- package/dist/tool-dialects.js +45 -0
- package/dist/tool-dialects.js.map +1 -1
- package/dist/winenv.js +26 -4
- package/dist/winenv.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ weaker (often free) models.
|
|
|
11
11
|
```bash
|
|
12
12
|
npm install -g llm-relay
|
|
13
13
|
llm-relay onboard # collect free provider keys (NIM, Groq, Gemini, OpenRouter, ...)
|
|
14
|
+
llm-relay onboard --import keys.env # import dotenv or a FreeLLMAPI export JSON
|
|
14
15
|
llm-relay setup claude-desktop # or: llm-relay setup claude-cli
|
|
15
16
|
llm-relay # start the proxy — leave it running
|
|
16
17
|
```
|
package/config.example.json
CHANGED
|
@@ -77,7 +77,8 @@
|
|
|
77
77
|
"offload": {
|
|
78
78
|
"claude": { "enabled": false, "scope": "subagents" },
|
|
79
79
|
"codex": { "enabled": false, "scope": "subagents" }
|
|
80
|
-
}
|
|
80
|
+
},
|
|
81
|
+
"sticky": false
|
|
81
82
|
},
|
|
82
83
|
"mode": "repair",
|
|
83
84
|
"repair": {
|
|
@@ -89,6 +90,8 @@
|
|
|
89
90
|
"Edit",
|
|
90
91
|
"MultiEdit",
|
|
91
92
|
"NotebookEdit",
|
|
93
|
+
"shell_command",
|
|
94
|
+
"apply_patch",
|
|
92
95
|
"rm",
|
|
93
96
|
"delete",
|
|
94
97
|
"delete_file",
|
package/dist/anthropic.d.ts
CHANGED
|
@@ -59,8 +59,8 @@ export interface AssistantMessage {
|
|
|
59
59
|
}
|
|
60
60
|
export declare function isToolUseBlock(b: ContentBlock): b is ToolUseBlock;
|
|
61
61
|
/**
|
|
62
|
-
* Extract the tools[] map (name →
|
|
63
|
-
* Value is `null` for a tool that is DECLARED but has no
|
|
62
|
+
* Extract the tools[] map (name → JSON schema) from a parsed Anthropic, Chat, or
|
|
63
|
+
* Responses request body. Value is `null` for a tool that is DECLARED but has no schema —
|
|
64
64
|
* notably Anthropic's built-in/typed tools (`bash`, `text_editor`, `computer`,
|
|
65
65
|
* `web_search`), which carry a `type` but no schema. Such tools are "known but
|
|
66
66
|
* unvalidatable": a tool_use naming them must NOT be flagged unknown_tool, but
|
package/dist/anthropic.js
CHANGED
|
@@ -8,8 +8,8 @@ export function isToolUseBlock(b) {
|
|
|
8
8
|
return b.type === "tool_use";
|
|
9
9
|
}
|
|
10
10
|
/**
|
|
11
|
-
* Extract the tools[] map (name →
|
|
12
|
-
* Value is `null` for a tool that is DECLARED but has no
|
|
11
|
+
* Extract the tools[] map (name → JSON schema) from a parsed Anthropic, Chat, or
|
|
12
|
+
* Responses request body. Value is `null` for a tool that is DECLARED but has no schema —
|
|
13
13
|
* notably Anthropic's built-in/typed tools (`bash`, `text_editor`, `computer`,
|
|
14
14
|
* `web_search`), which carry a `type` but no schema. Such tools are "known but
|
|
15
15
|
* unvalidatable": a tool_use naming them must NOT be flagged unknown_tool, but
|
|
@@ -21,12 +21,29 @@ export function toolSchemaMap(requestBody) {
|
|
|
21
21
|
requestBody !== null &&
|
|
22
22
|
Array.isArray(requestBody.tools)) {
|
|
23
23
|
for (const t of requestBody.tools) {
|
|
24
|
-
if (typeof t
|
|
25
|
-
|
|
24
|
+
if (typeof t !== "object" || t === null || Array.isArray(t))
|
|
25
|
+
continue;
|
|
26
|
+
const tool = t;
|
|
27
|
+
// Anthropic Messages and OpenAI Responses both put the function name on the tool itself;
|
|
28
|
+
// their schema fields differ. Anthropic built-ins deliberately remain declared/null.
|
|
29
|
+
if (typeof tool.name === "string") {
|
|
30
|
+
const rawSchema = tool.input_schema ?? tool.parameters;
|
|
26
31
|
const schema = typeof rawSchema === "object" && rawSchema !== null
|
|
27
32
|
? rawSchema
|
|
28
33
|
: null;
|
|
29
|
-
map.set(
|
|
34
|
+
map.set(tool.name, schema);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
// Chat Completions wraps declarations in `{ type, function: { name, parameters } }`.
|
|
38
|
+
// Reading only Anthropic's shape made every direct Chat tool request look tool-free.
|
|
39
|
+
if (tool.type === "function" && typeof tool.function === "object" && tool.function !== null) {
|
|
40
|
+
const fn = tool.function;
|
|
41
|
+
if (typeof fn.name !== "string")
|
|
42
|
+
continue;
|
|
43
|
+
const schema = typeof fn.parameters === "object" && fn.parameters !== null
|
|
44
|
+
? fn.parameters
|
|
45
|
+
: null;
|
|
46
|
+
map.set(fn.name, schema);
|
|
30
47
|
}
|
|
31
48
|
}
|
|
32
49
|
}
|
package/dist/anthropic.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"anthropic.js","sourceRoot":"","sources":["../src/anthropic.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAkEH,MAAM,UAAU,cAAc,CAAC,CAAe;IAC5C,OAAO,CAAC,CAAC,IAAI,KAAK,UAAU,CAAC;AAC/B,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAAC,WAAoB;IAChD,MAAM,GAAG,GAAG,IAAI,GAAG,EAA6B,CAAC;IACjD,IACE,OAAO,WAAW,KAAK,QAAQ;QAC/B,WAAW,KAAK,IAAI;QACpB,KAAK,CAAC,OAAO,CAAE,WAAmC,CAAC,KAAK,CAAC,EACzD,CAAC;QACD,KAAK,MAAM,CAAC,IAAK,WAAoC,CAAC,KAAK,EAAE,CAAC;YAC5D,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI,IAAI,
|
|
1
|
+
{"version":3,"file":"anthropic.js","sourceRoot":"","sources":["../src/anthropic.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAkEH,MAAM,UAAU,cAAc,CAAC,CAAe;IAC5C,OAAO,CAAC,CAAC,IAAI,KAAK,UAAU,CAAC;AAC/B,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAAC,WAAoB;IAChD,MAAM,GAAG,GAAG,IAAI,GAAG,EAA6B,CAAC;IACjD,IACE,OAAO,WAAW,KAAK,QAAQ;QAC/B,WAAW,KAAK,IAAI;QACpB,KAAK,CAAC,OAAO,CAAE,WAAmC,CAAC,KAAK,CAAC,EACzD,CAAC;QACD,KAAK,MAAM,CAAC,IAAK,WAAoC,CAAC,KAAK,EAAE,CAAC;YAC5D,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;gBAAE,SAAS;YACtE,MAAM,IAAI,GAAG,CAA4B,CAAC;YAE1C,yFAAyF;YACzF,qFAAqF;YACrF,IAAI,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;gBAClC,MAAM,SAAS,GAAG,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC,UAAU,CAAC;gBACvD,MAAM,MAAM,GACV,OAAO,SAAS,KAAK,QAAQ,IAAI,SAAS,KAAK,IAAI;oBACjD,CAAC,CAAE,SAAwB;oBAC3B,CAAC,CAAC,IAAI,CAAC;gBACX,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;gBAC3B,SAAS;YACX,CAAC;YAED,qFAAqF;YACrF,qFAAqF;YACrF,IAAI,IAAI,CAAC,IAAI,KAAK,UAAU,IAAI,OAAO,IAAI,CAAC,QAAQ,KAAK,QAAQ,IAAI,IAAI,CAAC,QAAQ,KAAK,IAAI,EAAE,CAAC;gBAC5F,MAAM,EAAE,GAAG,IAAI,CAAC,QAAmC,CAAC;gBACpD,IAAI,OAAO,EAAE,CAAC,IAAI,KAAK,QAAQ;oBAAE,SAAS;gBAC1C,MAAM,MAAM,GACV,OAAO,EAAE,CAAC,UAAU,KAAK,QAAQ,IAAI,EAAE,CAAC,UAAU,KAAK,IAAI;oBACzD,CAAC,CAAE,EAAE,CAAC,UAAyB;oBAC/B,CAAC,CAAC,IAAI,CAAC;gBACX,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;YAC3B,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
package/dist/authEnv.d.ts
CHANGED
|
@@ -12,6 +12,13 @@
|
|
|
12
12
|
* key-shaped. A heuristic match could ship one provider's credential to another
|
|
13
13
|
* provider's endpoint, which is a credential leak, not a convenience.
|
|
14
14
|
*/
|
|
15
|
+
/**
|
|
16
|
+
* The curated aliases only, without the provider-name-derived candidates used by
|
|
17
|
+
* `candidateEnvNames()`. Importers need this narrower surface: accepting a credential from a
|
|
18
|
+
* file is allowed only when its NAME is explicitly listed, never because its value happens to
|
|
19
|
+
* look like a key or because an arbitrary provider slug can be turned into a plausible env name.
|
|
20
|
+
*/
|
|
21
|
+
export declare function curatedEnvNames(providerName: string): string[];
|
|
15
22
|
/**
|
|
16
23
|
* Every env-var name that may carry this provider's credential, most-preferred first:
|
|
17
24
|
* the declared name, then curated aliases, then names derived from the provider name.
|
package/dist/authEnv.js
CHANGED
|
@@ -31,6 +31,15 @@ const PROVIDER_ENV_ALIASES = {
|
|
|
31
31
|
openai: ["OPENAI_API_KEY"],
|
|
32
32
|
anthropic: ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"],
|
|
33
33
|
};
|
|
34
|
+
/**
|
|
35
|
+
* The curated aliases only, without the provider-name-derived candidates used by
|
|
36
|
+
* `candidateEnvNames()`. Importers need this narrower surface: accepting a credential from a
|
|
37
|
+
* file is allowed only when its NAME is explicitly listed, never because its value happens to
|
|
38
|
+
* look like a key or because an arbitrary provider slug can be turned into a plausible env name.
|
|
39
|
+
*/
|
|
40
|
+
export function curatedEnvNames(providerName) {
|
|
41
|
+
return [...(PROVIDER_ENV_ALIASES[providerName.toLowerCase()] ?? [])];
|
|
42
|
+
}
|
|
34
43
|
/** `my-provider.2` → `MY_PROVIDER_2`, so a custom provider gets sane derived candidates. */
|
|
35
44
|
function slug(providerName) {
|
|
36
45
|
return providerName.toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
package/dist/authEnv.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"authEnv.js","sourceRoot":"","sources":["../src/authEnv.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"authEnv.js","sourceRoot":"","sources":["../src/authEnv.ts"],"names":[],"mappings":"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"}
|
package/dist/backend.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ResolvedTarget } from "./config.js";
|
|
2
|
+
import { type RecoveredOpenAiChatProcessor } from "./openai-dialect.js";
|
|
2
3
|
/**
|
|
3
4
|
* A tool-call envelope was present in the response text but could not be parsed — truncated, or a
|
|
4
5
|
* dialect variant we do not model. Distinct from a mapper defect: the relay's translation is fine,
|
|
@@ -83,6 +84,8 @@ export declare const DEGRADED_HEADER = "x-llm-relay-degraded";
|
|
|
83
84
|
* `openrouter/anthropic/claude-sonnet-5 (paid, published-price)`.
|
|
84
85
|
*/
|
|
85
86
|
export declare const PAID_HEADER = "x-llm-relay-paid";
|
|
87
|
+
/** This response contains a tool call reconstructed from a recognized text dialect envelope. */
|
|
88
|
+
export declare const TOOL_DIALECT_HEADER = "x-llm-relay-tool-dialect";
|
|
86
89
|
/**
|
|
87
90
|
* The provider's `Retry-After` in milliseconds, or null.
|
|
88
91
|
*
|
|
@@ -93,6 +96,8 @@ export declare const PAID_HEADER = "x-llm-relay-paid";
|
|
|
93
96
|
* garbage as 0 would silently disable the backoff this exists to honour.
|
|
94
97
|
*/
|
|
95
98
|
export declare function parseRetryAfterMs(value: string | null | undefined, now?: number): number | null;
|
|
99
|
+
/** Raw model id stated by the upstream response, before any relay translation. */
|
|
100
|
+
export declare function upstreamReportedModel(response: Response): string | undefined;
|
|
96
101
|
/**
|
|
97
102
|
* Fetch the resolved provider target and return an ANTHROPIC-shaped `Response`,
|
|
98
103
|
* regardless of the backend's native wire format. For kind="anthropic" this is a
|
|
@@ -170,4 +175,5 @@ export declare function fetchOpenAiFront(target: ResolvedTarget, args: {
|
|
|
170
175
|
signal: AbortSignal;
|
|
171
176
|
protocol?: OpenAiFrontProtocol;
|
|
172
177
|
anthropicHeaders?: Record<string, string>;
|
|
178
|
+
processRecoveredChat?: RecoveredOpenAiChatProcessor;
|
|
173
179
|
}, fetchFn?: typeof fetch): Promise<Response>;
|
package/dist/backend.js
CHANGED
|
@@ -4,6 +4,9 @@ import { DocumentError, transcodeDocuments } from "./documents.js";
|
|
|
4
4
|
import { recoverToolCalls } from "./tool-dialects.js";
|
|
5
5
|
import { recoverDialectInStream } from "./dialect-stream.js";
|
|
6
6
|
import { toolSchemaMap } from "./anthropic.js";
|
|
7
|
+
import { stripOpeningThinkTag, stripThinkTagsInStream } from "./think-tags.js";
|
|
8
|
+
import { STREAM_PREFLIGHT_LIMIT } from "./stream-commit.js";
|
|
9
|
+
import { inspectDialectInOpenAiChat, recoverDialectInOpenAiChatStream, } from "./openai-dialect.js";
|
|
7
10
|
/**
|
|
8
11
|
* A tool-call envelope was present in the response text but could not be parsed — truncated, or a
|
|
9
12
|
* dialect variant we do not model. Distinct from a mapper defect: the relay's translation is fine,
|
|
@@ -93,6 +96,8 @@ export const DEGRADED_HEADER = "x-llm-relay-degraded";
|
|
|
93
96
|
* `openrouter/anthropic/claude-sonnet-5 (paid, published-price)`.
|
|
94
97
|
*/
|
|
95
98
|
export const PAID_HEADER = "x-llm-relay-paid";
|
|
99
|
+
/** This response contains a tool call reconstructed from a recognized text dialect envelope. */
|
|
100
|
+
export const TOOL_DIALECT_HEADER = "x-llm-relay-tool-dialect";
|
|
96
101
|
/**
|
|
97
102
|
* The provider's `Retry-After` in milliseconds, or null.
|
|
98
103
|
*
|
|
@@ -119,7 +124,17 @@ export function parseRetryAfterMs(value, now = Date.now()) {
|
|
|
119
124
|
const delta = at - now;
|
|
120
125
|
return delta > 0 ? delta : null;
|
|
121
126
|
}
|
|
122
|
-
|
|
127
|
+
// Response provenance is private process state, not a wire header: callers can
|
|
128
|
+
// read it for logging without accidentally forwarding it to the client.
|
|
129
|
+
const upstreamResponseMetadata = new WeakMap();
|
|
130
|
+
/** Raw model id stated by the upstream response, before any relay translation. */
|
|
131
|
+
export function upstreamReportedModel(response) {
|
|
132
|
+
return upstreamResponseMetadata.get(response)?.reportedModel;
|
|
133
|
+
}
|
|
134
|
+
function attachUpstreamMetadata(response, metadata) {
|
|
135
|
+
upstreamResponseMetadata.set(response, metadata);
|
|
136
|
+
return response;
|
|
137
|
+
}
|
|
123
138
|
function isRecord(value) {
|
|
124
139
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
125
140
|
}
|
|
@@ -132,10 +147,19 @@ function isRecord(value) {
|
|
|
132
147
|
function invalidEnvelopeReason(value, protocol, streamed) {
|
|
133
148
|
if (!isRecord(value))
|
|
134
149
|
return "expected a JSON object";
|
|
135
|
-
// An error
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
150
|
+
// An error envelope seen here under `streamed` is the FIRST event of a 2xx stream — preflight
|
|
151
|
+
// inspects nothing later, so this cannot fire mid-stream. Pre-commit, an in-band error frame
|
|
152
|
+
// is a dead turn, not a response: failing it makes the candidate loop walk on to a member
|
|
153
|
+
// that answers, where forwarding it spent the whole pool on one member's error inside a 200
|
|
154
|
+
// (adoption review §1.1). An error frame arriving AFTER a valid first event still streams
|
|
155
|
+
// through untouched — post-commit, honesty beats replay. The reason carries a bounded excerpt
|
|
156
|
+
// of the upstream's own message, same maxim as serving the last candidate's real error.
|
|
157
|
+
if (streamed && isRecord(value.error)) {
|
|
158
|
+
const message = typeof value.error.message === "string" ? value.error.message.slice(0, 200) : "";
|
|
159
|
+
return message
|
|
160
|
+
? `stream opened with an in-band error event: ${message}`
|
|
161
|
+
: "stream opened with an in-band error event";
|
|
162
|
+
}
|
|
139
163
|
if (protocol === "openai-chat") {
|
|
140
164
|
if (!Array.isArray(value.choices))
|
|
141
165
|
return "missing choices array";
|
|
@@ -201,16 +225,43 @@ function invalidEnvelopeReason(value, protocol, streamed) {
|
|
|
201
225
|
return "missing or unknown Anthropic event type";
|
|
202
226
|
}
|
|
203
227
|
}
|
|
228
|
+
function captureReportedModel(metadata, value, protocol, streamed) {
|
|
229
|
+
if (metadata.reportedModel !== undefined || !isRecord(value))
|
|
230
|
+
return;
|
|
231
|
+
const envelope = protocol === "anthropic-messages" && streamed && value.type === "message_start"
|
|
232
|
+
? value.message
|
|
233
|
+
: value;
|
|
234
|
+
if (isRecord(envelope) && typeof envelope.model === "string") {
|
|
235
|
+
metadata.reportedModel = envelope.model;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
204
238
|
/** Inspect the first data event before handing a provider stream to llm-bridge. */
|
|
205
239
|
async function preflightResponseStream(body, protocol) {
|
|
206
240
|
const reader = body.getReader();
|
|
207
241
|
const chunks = [];
|
|
208
242
|
const decoder = new TextDecoder();
|
|
243
|
+
const metadata = {};
|
|
209
244
|
let buffered = "";
|
|
210
245
|
let byteLength = 0;
|
|
246
|
+
const captureCompleteEvents = (final = false) => {
|
|
247
|
+
let boundary;
|
|
248
|
+
const separator = /\r?\n\r?\n/g;
|
|
249
|
+
while ((boundary = separator.exec(buffered)) !== null) {
|
|
250
|
+
const event = buffered.slice(0, boundary.index);
|
|
251
|
+
buffered = buffered.slice(boundary.index + boundary[0].length);
|
|
252
|
+
separator.lastIndex = 0;
|
|
253
|
+
captureEventModel(event);
|
|
254
|
+
}
|
|
255
|
+
if (final && buffered.trim())
|
|
256
|
+
captureEventModel(buffered);
|
|
257
|
+
};
|
|
211
258
|
const replay = () => {
|
|
212
259
|
let prefixIndex = 0;
|
|
213
|
-
|
|
260
|
+
// The first valid event can be a ping. Keep observing the untouched raw
|
|
261
|
+
// stream while the consumer drains it so a later message_start/chunk can
|
|
262
|
+
// still supply the upstream's model before terminal logging.
|
|
263
|
+
captureCompleteEvents();
|
|
264
|
+
return { ok: true, metadata, body: new ReadableStream({
|
|
214
265
|
async pull(controller) {
|
|
215
266
|
if (prefixIndex < chunks.length) {
|
|
216
267
|
controller.enqueue(chunks[prefixIndex++]);
|
|
@@ -218,10 +269,16 @@ async function preflightResponseStream(body, protocol) {
|
|
|
218
269
|
}
|
|
219
270
|
try {
|
|
220
271
|
const more = await reader.read();
|
|
221
|
-
if (more.done)
|
|
272
|
+
if (more.done) {
|
|
273
|
+
buffered += decoder.decode();
|
|
274
|
+
captureCompleteEvents(true);
|
|
222
275
|
controller.close();
|
|
223
|
-
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
buffered += decoder.decode(more.value, { stream: true });
|
|
279
|
+
captureCompleteEvents();
|
|
224
280
|
controller.enqueue(more.value);
|
|
281
|
+
}
|
|
225
282
|
}
|
|
226
283
|
catch (error) {
|
|
227
284
|
controller.error(error);
|
|
@@ -254,8 +311,25 @@ async function preflightResponseStream(body, protocol) {
|
|
|
254
311
|
catch {
|
|
255
312
|
return "data event is not valid JSON";
|
|
256
313
|
}
|
|
314
|
+
captureReportedModel(metadata, parsed, protocol, true);
|
|
257
315
|
return invalidEnvelopeReason(parsed, protocol, true);
|
|
258
316
|
};
|
|
317
|
+
const captureEventModel = (event) => {
|
|
318
|
+
const data = event
|
|
319
|
+
.split(/\r?\n/)
|
|
320
|
+
.filter((line) => line.startsWith("data:"))
|
|
321
|
+
.map((line) => line.slice(5).trimStart())
|
|
322
|
+
.join("\n")
|
|
323
|
+
.trim();
|
|
324
|
+
if (!data || data === "[DONE]")
|
|
325
|
+
return;
|
|
326
|
+
try {
|
|
327
|
+
captureReportedModel(metadata, JSON.parse(data), protocol, true);
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
// Preflight owns envelope validity; this observer owns provenance only.
|
|
331
|
+
}
|
|
332
|
+
};
|
|
259
333
|
while (byteLength <= STREAM_PREFLIGHT_LIMIT) {
|
|
260
334
|
const next = await reader.read().catch(() => null);
|
|
261
335
|
if (next === null)
|
|
@@ -267,7 +341,9 @@ async function preflightResponseStream(body, protocol) {
|
|
|
267
341
|
// completion. Preserve the old adapter behaviour for that genuine envelope.
|
|
268
342
|
if (finalReason === undefined && buffered.trim()) {
|
|
269
343
|
try {
|
|
270
|
-
|
|
344
|
+
const parsed = JSON.parse(buffered);
|
|
345
|
+
captureReportedModel(metadata, parsed, protocol, false);
|
|
346
|
+
finalReason = invalidEnvelopeReason(parsed, protocol, false);
|
|
271
347
|
}
|
|
272
348
|
catch {
|
|
273
349
|
// The SSE-specific reason below remains more useful.
|
|
@@ -330,7 +406,7 @@ export async function fetchBackend(target, args, fetchFn = fetch) {
|
|
|
330
406
|
...retryAfterHeader(res.headers),
|
|
331
407
|
}, "invalid_upstream_envelope");
|
|
332
408
|
}
|
|
333
|
-
return new Response(preflight.body, { status: res.status, headers: res.headers });
|
|
409
|
+
return attachUpstreamMetadata(new Response(preflight.body, { status: res.status, headers: res.headers }), preflight.metadata);
|
|
334
410
|
}
|
|
335
411
|
let body;
|
|
336
412
|
try {
|
|
@@ -347,7 +423,9 @@ export async function fetchBackend(target, args, fetchFn = fetch) {
|
|
|
347
423
|
...retryAfterHeader(res.headers),
|
|
348
424
|
}, "invalid_upstream_envelope");
|
|
349
425
|
}
|
|
350
|
-
|
|
426
|
+
const metadata = {};
|
|
427
|
+
captureReportedModel(metadata, body, "anthropic-messages", false);
|
|
428
|
+
return attachUpstreamMetadata(res, metadata);
|
|
351
429
|
}
|
|
352
430
|
// kind === "openai"
|
|
353
431
|
// llm-bridge stringifies any block type it doesn't know, which would put a document's
|
|
@@ -421,12 +499,16 @@ export async function fetchBackend(target, args, fetchFn = fetch) {
|
|
|
421
499
|
}
|
|
422
500
|
try {
|
|
423
501
|
const anthStream = handleUniversalStreamRequest(preflight.body, "openai", "anthropic");
|
|
502
|
+
// Strip a complete message-opening think block BEFORE dialect scanning. Otherwise the
|
|
503
|
+
// preamble can hide a tool envelope from recovery. Native Anthropic streams never enter
|
|
504
|
+
// this branch, and direct OpenAI Chat passthrough is handled on the other front.
|
|
505
|
+
const strippedStream = stripThinkTagsInStream(anthStream);
|
|
424
506
|
// Recover a tool call this host returned as raw dialect TEXT. Gated on the request actually
|
|
425
507
|
// declaring tools: with none declared there is no call to recover, and wrapping the stream
|
|
426
508
|
// would add holdback latency for nothing.
|
|
427
509
|
const schemas = toolSchemaMap(args.reqJson);
|
|
428
|
-
const body = schemas.size > 0 ? recoverDialectInStream(
|
|
429
|
-
return new Response(body, { status: res.status, headers: { "content-type": "text/event-stream" } });
|
|
510
|
+
const body = schemas.size > 0 ? recoverDialectInStream(strippedStream, schemas) : strippedStream;
|
|
511
|
+
return attachUpstreamMetadata(new Response(body, { status: res.status, headers: { "content-type": "text/event-stream" } }), preflight.metadata);
|
|
430
512
|
}
|
|
431
513
|
catch (e) {
|
|
432
514
|
return anthropicError(502, `llm-relay: response translation failed: ${e.message}`, "local", {}, "relay_mapper_defect");
|
|
@@ -467,13 +549,16 @@ export async function fetchBackend(target, args, fetchFn = fetch) {
|
|
|
467
549
|
// whose tool call the RELAY reconstructed is not the same as one that arrived correct, and an
|
|
468
550
|
// unflagged reconstruction is indistinguishable from a host that worked.
|
|
469
551
|
const recoveredDialect = recoveredDialectOf(anthropicJson);
|
|
470
|
-
|
|
552
|
+
const response = new Response(JSON.stringify(anthropicJson), {
|
|
471
553
|
status: 200,
|
|
472
554
|
headers: {
|
|
473
555
|
"content-type": "application/json",
|
|
474
|
-
...(recoveredDialect ? {
|
|
556
|
+
...(recoveredDialect ? { [TOOL_DIALECT_HEADER]: recoveredDialect } : {}),
|
|
475
557
|
},
|
|
476
558
|
});
|
|
559
|
+
const metadata = {};
|
|
560
|
+
captureReportedModel(metadata, upstreamJson, "openai-chat", false);
|
|
561
|
+
return attachUpstreamMetadata(response, metadata);
|
|
477
562
|
}
|
|
478
563
|
/** Did this translated message carry a tool call the relay reconstructed from text? */
|
|
479
564
|
function recoveredDialectOf(message) {
|
|
@@ -495,11 +580,14 @@ export function openAiResponseToAnthropic(j, model, schemas) {
|
|
|
495
580
|
const msg = choice.message ?? {};
|
|
496
581
|
const content = [];
|
|
497
582
|
let toolCalls = msg.tool_calls ?? [];
|
|
583
|
+
// This mapper is the translated OpenAI→Anthropic seam. Strip first so an opening think block
|
|
584
|
+
// cannot conceal a following dialect envelope from the recovery pass below.
|
|
585
|
+
const messageText = typeof msg.content === "string" ? stripOpeningThinkTag(msg.content) : null;
|
|
498
586
|
// Recover ONLY when the host parsed nothing. A host that populated `tool_calls` has already
|
|
499
587
|
// spoken; second-guessing it here would be inference, not translation.
|
|
500
588
|
let recovered = [];
|
|
501
|
-
if (toolCalls.length === 0 &&
|
|
502
|
-
const out = recoverToolCalls(
|
|
589
|
+
if (toolCalls.length === 0 && messageText !== null && messageText.length > 0) {
|
|
590
|
+
const out = recoverToolCalls(messageText, schemas ?? new Map());
|
|
503
591
|
if (out.status === "parsed") {
|
|
504
592
|
recovered = out.calls;
|
|
505
593
|
if (out.text.length > 0)
|
|
@@ -513,11 +601,11 @@ export function openAiResponseToAnthropic(j, model, schemas) {
|
|
|
513
601
|
throw new DialectUnparseableError(out.dialect);
|
|
514
602
|
}
|
|
515
603
|
else {
|
|
516
|
-
content.push({ type: "text", text:
|
|
604
|
+
content.push({ type: "text", text: messageText });
|
|
517
605
|
}
|
|
518
606
|
}
|
|
519
|
-
else if (
|
|
520
|
-
content.push({ type: "text", text:
|
|
607
|
+
else if (messageText !== null && messageText.length > 0) {
|
|
608
|
+
content.push({ type: "text", text: messageText });
|
|
521
609
|
}
|
|
522
610
|
if (recovered.length > 0) {
|
|
523
611
|
toolCalls = recovered.map((c, i) => ({
|
|
@@ -773,6 +861,7 @@ export async function fetchOpenAiFront(target, args, fetchFn = fetch) {
|
|
|
773
861
|
// Preserve the existing direct path for the protocol/backend pair that already speaks the
|
|
774
862
|
// same wire format. It keeps provider-specific OpenAI fields byte-for-byte intact.
|
|
775
863
|
if (target.kind === "openai" && protocol === "chat") {
|
|
864
|
+
const schemas = toolSchemaMap(base);
|
|
776
865
|
const body = { ...base, model: target.model, stream: args.wantsStream };
|
|
777
866
|
const res = await fetchFn(target.base + "/chat/completions", {
|
|
778
867
|
method: "POST",
|
|
@@ -791,7 +880,18 @@ export async function fetchOpenAiFront(target, args, fetchFn = fetch) {
|
|
|
791
880
|
if (!preflight.ok) {
|
|
792
881
|
return openaiError(502, `llm-relay: invalid OpenAI upstream envelope: ${preflight.reason}`, "upstream", "invalid_upstream_envelope", retryAfterHeader(res.headers));
|
|
793
882
|
}
|
|
794
|
-
|
|
883
|
+
let response = null;
|
|
884
|
+
let recovered = false;
|
|
885
|
+
const responseBody = schemas.size > 0
|
|
886
|
+
? recoverDialectInOpenAiChatStream(preflight.body, schemas, () => {
|
|
887
|
+
recovered = true;
|
|
888
|
+
response?.headers.set(TOOL_DIALECT_HEADER, "recovered");
|
|
889
|
+
}, args.processRecoveredChat)
|
|
890
|
+
: preflight.body;
|
|
891
|
+
response = new Response(responseBody, { status: res.status, headers: res.headers });
|
|
892
|
+
if (recovered)
|
|
893
|
+
response.headers.set(TOOL_DIALECT_HEADER, "recovered");
|
|
894
|
+
return attachUpstreamMetadata(response, preflight.metadata);
|
|
795
895
|
}
|
|
796
896
|
let responseBody;
|
|
797
897
|
try {
|
|
@@ -804,7 +904,28 @@ export async function fetchOpenAiFront(target, args, fetchFn = fetch) {
|
|
|
804
904
|
if (invalidReason) {
|
|
805
905
|
return openaiError(502, `llm-relay: invalid OpenAI upstream envelope: ${invalidReason}`, "upstream", "invalid_upstream_envelope", retryAfterHeader(res.headers));
|
|
806
906
|
}
|
|
807
|
-
|
|
907
|
+
let recovery;
|
|
908
|
+
try {
|
|
909
|
+
recovery = await inspectDialectInOpenAiChat(responseBody, schemas, args.processRecoveredChat);
|
|
910
|
+
}
|
|
911
|
+
catch (error) {
|
|
912
|
+
return openaiError(502, `llm-relay: ${error instanceof Error ? error.message : String(error)}`, "upstream", "tool_call_recovery_failed");
|
|
913
|
+
}
|
|
914
|
+
if (recovery.status === "detected") {
|
|
915
|
+
return openaiError(502, `llm-relay: backend returned an unparseable ${recovery.dialect} tool-call envelope as text`, "upstream", "tool_dialect_unparseable");
|
|
916
|
+
}
|
|
917
|
+
const metadata = {};
|
|
918
|
+
const finalBody = recovery.status === "parsed" ? recovery.body : responseBody;
|
|
919
|
+
captureReportedModel(metadata, finalBody, "openai-chat", false);
|
|
920
|
+
if (recovery.status === "parsed") {
|
|
921
|
+
const headers = new Headers(res.headers);
|
|
922
|
+
headers.set(TOOL_DIALECT_HEADER, "recovered");
|
|
923
|
+
return attachUpstreamMetadata(new Response(JSON.stringify(finalBody), {
|
|
924
|
+
status: res.status,
|
|
925
|
+
headers,
|
|
926
|
+
}), metadata);
|
|
927
|
+
}
|
|
928
|
+
return attachUpstreamMetadata(res, metadata);
|
|
808
929
|
}
|
|
809
930
|
let anthropicBody;
|
|
810
931
|
try {
|
|
@@ -827,6 +948,7 @@ export async function fetchOpenAiFront(target, args, fetchFn = fetch) {
|
|
|
827
948
|
wantsStream: args.wantsStream,
|
|
828
949
|
signal: args.signal,
|
|
829
950
|
}, fetchFn);
|
|
951
|
+
const metadata = upstreamResponseMetadata.get(backendRes);
|
|
830
952
|
if (!backendRes.ok) {
|
|
831
953
|
const raw = await backendRes.text().catch(() => "");
|
|
832
954
|
const origin = errorOrigin(backendRes) ?? "upstream";
|
|
@@ -853,7 +975,8 @@ export async function fetchOpenAiFront(target, args, fetchFn = fetch) {
|
|
|
853
975
|
const targetProtocol = protocol === "responses" ? "openai-responses" : "openai";
|
|
854
976
|
try {
|
|
855
977
|
const output = handleUniversalStreamRequest(preflight.body, "anthropic", targetProtocol);
|
|
856
|
-
|
|
978
|
+
const response = new Response(output, { status: backendRes.status, headers: { "content-type": "text/event-stream" } });
|
|
979
|
+
return metadata ? attachUpstreamMetadata(response, metadata) : response;
|
|
857
980
|
}
|
|
858
981
|
catch (e) {
|
|
859
982
|
return openaiError(502, `llm-relay: response translation failed: ${e.message}`, "local", "relay_mapper_defect");
|
|
@@ -875,10 +998,11 @@ export async function fetchOpenAiFront(target, args, fetchFn = fetch) {
|
|
|
875
998
|
: `llm-relay: invalid Anthropic upstream envelope: ${invalidReason}`, generated ? "local" : "upstream", generated ? "relay_mapper_defect" : "invalid_upstream_envelope", retryAfterHeader(backendRes.headers));
|
|
876
999
|
}
|
|
877
1000
|
try {
|
|
878
|
-
|
|
1001
|
+
const response = new Response(JSON.stringify(anthropicMessageToOpenAi(body, protocol, target.model ?? String(base.model ?? ""))), {
|
|
879
1002
|
status: backendRes.status,
|
|
880
1003
|
headers: { "content-type": "application/json" },
|
|
881
1004
|
});
|
|
1005
|
+
return metadata ? attachUpstreamMetadata(response, metadata) : response;
|
|
882
1006
|
}
|
|
883
1007
|
catch (e) {
|
|
884
1008
|
return openaiError(502, `llm-relay: response translation failed: ${e.message}`, "local", "relay_mapper_defect");
|