llm-relay 0.35.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 +135 -20
- 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/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 +92 -77
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +14 -1
- package/dist/config.js +72 -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 +3 -3
- package/dist/onboarding.js +5 -3
- 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/refusal-interpretation.d.ts +4 -4
- package/dist/refusal-interpretation.js +6 -6
- package/dist/refusal-interpretation.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 +2 -0
- package/dist/server.js +441 -63
- 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/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":"AAAA;;;;;;;;;;;;;GAaG;AAEH,4FAA4F;AAC5F,MAAM,oBAAoB,GAA6B;IACrD,MAAM,EAAE;QACN,gBAAgB;QAChB,kBAAkB;QAClB,mBAAmB;QACnB,sBAAsB;QACtB,uBAAuB;QACvB,gBAAgB;KACjB;IACD,GAAG,EAAE,CAAC,gBAAgB,EAAE,oBAAoB,EAAE,aAAa,CAAC;IAC5D,UAAU,EAAE,CAAC,oBAAoB,EAAE,qBAAqB,CAAC;IACzD,IAAI,EAAE,CAAC,cAAc,CAAC;IACtB,OAAO,EAAE,CAAC,iBAAiB,EAAE,mBAAmB,CAAC;IACjD,QAAQ,EAAE,CAAC,kBAAkB,CAAC;IAC9B,SAAS,EAAE,CAAC,mBAAmB,CAAC;IAChC,MAAM,EAAE,CAAC,gBAAgB,CAAC;IAC1B,SAAS,EAAE,CAAC,mBAAmB,EAAE,sBAAsB,CAAC;CACzD,CAAC;AAEF;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,YAAoB;IAClD,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,YAAY,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;AACvE,CAAC;AAED,4FAA4F;AAC5F,SAAS,IAAI,CAAC,YAAoB;IAChC,OAAO,YAAY,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;AACxF,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,YAAoB,EAAE,QAAiB;IACvE,MAAM,CAAC,GAAG,IAAI,CAAC,YAAY,CAAC,CAAC;IAC7B,MAAM,OAAO,GAAG;QACd,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/B,GAAG,CAAC,oBAAoB,CAAC,YAAY,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC;QAC3D,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;KACzD,CAAC;IACF,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC;AAC/B,CAAC;AAWD;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAC,KAAyB;IACpD,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;AACzC,CAAC;AAiBD,MAAM,UAAU,oBAAoB,CAClC,eAAmC,EACnC,MAAyB,OAAO,CAAC,GAAG,EACpC,YAAqB;IAErB,IAAI,CAAC,eAAe;QAAE,OAAO,SAAS,CAAC;IACvC,IAAI,YAAY,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;QAAE,OAAO,eAAe,CAAC;IAE/D,IAAI,QAAQ,GAAG,YAAY,EAAE,WAAW,EAAE,CAAC;IAC3C,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,KAAK,MAAM,CAAC,CAAC,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,oBAAoB,CAAC,EAAE,CAAC;YAChE,IAAI,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC,EAAE,CAAC;gBACtC,QAAQ,GAAG,CAAC,CAAC;gBACb,MAAM;YACR,CAAC;QACH,CAAC;IACH,CAAC;IAED,IAAI,QAAQ,EAAE,CAAC;QACb,MAAM,GAAG,GAAG,cAAc,CAAC,QAAQ,EAAE,eAAe,EAAE,GAAG,CAAC,CAAC;QAC3D,IAAI,GAAG,CAAC,IAAI,IAAI,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;YAC5C,OAAO,GAAG,CAAC,IAAI,CAAC;QAClB,CAAC;IACH,CAAC;IAED,OAAO,eAAe,CAAC;AACzB,CAAC;AAED,MAAM,UAAU,eAAe,CAC7B,eAAmC,EACnC,MAAyB,OAAO,CAAC,GAAG,EACpC,YAAqB;IAErB,IAAI,CAAC,eAAe;QAAE,OAAO,cAAc,CAAC;IAC5C,MAAM,UAAU,GAAG,oBAAoB,CAAC,eAAe,EAAE,GAAG,EAAE,YAAY,CAAC,CAAC;IAC5E,OAAO,UAAU,IAAI,YAAY,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,kBAAkB,CAAC;AAC/F,CAAC;AAED,MAAM,UAAU,cAAc,CAC5B,eAAmC,EACnC,MAAyB,OAAO,CAAC,GAAG,EACpC,YAAqB;IAErB,IAAI,CAAC,eAAe;QAAE,OAAO,SAAS,CAAC;IACvC,MAAM,UAAU,GAAG,oBAAoB,CAAC,eAAe,EAAE,GAAG,EAAE,YAAY,CAAC,CAAC;IAC5E,MAAM,GAAG,GAAG,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACrD,OAAO,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;AACrD,CAAC;AAYD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,UAAU,gBAAgB,CAC9B,GAAuB,EACvB,UAA0B;IAE1B,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC;QAAE,OAAO,EAAE,CAAC;IAClC,MAAM,KAAK,GAAG,GAAI,CAAC,IAAI,EAAE,CAAC;IAC1B,IAAI,UAAU,KAAK,eAAe,EAAE,CAAC;QACnC,OAAO,EAAE,aAAa,EAAE,KAAK,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,UAAU,KAAK,EAAE,EAAE,CAAC;IACpF,CAAC;IACD,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC;AAChC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAC5B,YAAoB,EACpB,QAA4B,EAC5B,MAAyB,OAAO,CAAC,GAAG;IAEpC,MAAM,UAAU,GAAG,iBAAiB,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAC;IAC7D,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3D,OAAO;QACL,IAAI,EAAE,KAAK,IAAI,QAAQ;QACvB,QAAQ,EAAE,OAAO,CAAC,KAAK,IAAI,KAAK,KAAK,QAAQ,CAAC;QAC9C,UAAU;KACX,CAAC;AACJ,CAAC"}
|
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
|
}
|
|
@@ -210,16 +225,43 @@ function invalidEnvelopeReason(value, protocol, streamed) {
|
|
|
210
225
|
return "missing or unknown Anthropic event type";
|
|
211
226
|
}
|
|
212
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
|
+
}
|
|
213
238
|
/** Inspect the first data event before handing a provider stream to llm-bridge. */
|
|
214
239
|
async function preflightResponseStream(body, protocol) {
|
|
215
240
|
const reader = body.getReader();
|
|
216
241
|
const chunks = [];
|
|
217
242
|
const decoder = new TextDecoder();
|
|
243
|
+
const metadata = {};
|
|
218
244
|
let buffered = "";
|
|
219
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
|
+
};
|
|
220
258
|
const replay = () => {
|
|
221
259
|
let prefixIndex = 0;
|
|
222
|
-
|
|
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({
|
|
223
265
|
async pull(controller) {
|
|
224
266
|
if (prefixIndex < chunks.length) {
|
|
225
267
|
controller.enqueue(chunks[prefixIndex++]);
|
|
@@ -227,10 +269,16 @@ async function preflightResponseStream(body, protocol) {
|
|
|
227
269
|
}
|
|
228
270
|
try {
|
|
229
271
|
const more = await reader.read();
|
|
230
|
-
if (more.done)
|
|
272
|
+
if (more.done) {
|
|
273
|
+
buffered += decoder.decode();
|
|
274
|
+
captureCompleteEvents(true);
|
|
231
275
|
controller.close();
|
|
232
|
-
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
buffered += decoder.decode(more.value, { stream: true });
|
|
279
|
+
captureCompleteEvents();
|
|
233
280
|
controller.enqueue(more.value);
|
|
281
|
+
}
|
|
234
282
|
}
|
|
235
283
|
catch (error) {
|
|
236
284
|
controller.error(error);
|
|
@@ -263,8 +311,25 @@ async function preflightResponseStream(body, protocol) {
|
|
|
263
311
|
catch {
|
|
264
312
|
return "data event is not valid JSON";
|
|
265
313
|
}
|
|
314
|
+
captureReportedModel(metadata, parsed, protocol, true);
|
|
266
315
|
return invalidEnvelopeReason(parsed, protocol, true);
|
|
267
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
|
+
};
|
|
268
333
|
while (byteLength <= STREAM_PREFLIGHT_LIMIT) {
|
|
269
334
|
const next = await reader.read().catch(() => null);
|
|
270
335
|
if (next === null)
|
|
@@ -276,7 +341,9 @@ async function preflightResponseStream(body, protocol) {
|
|
|
276
341
|
// completion. Preserve the old adapter behaviour for that genuine envelope.
|
|
277
342
|
if (finalReason === undefined && buffered.trim()) {
|
|
278
343
|
try {
|
|
279
|
-
|
|
344
|
+
const parsed = JSON.parse(buffered);
|
|
345
|
+
captureReportedModel(metadata, parsed, protocol, false);
|
|
346
|
+
finalReason = invalidEnvelopeReason(parsed, protocol, false);
|
|
280
347
|
}
|
|
281
348
|
catch {
|
|
282
349
|
// The SSE-specific reason below remains more useful.
|
|
@@ -339,7 +406,7 @@ export async function fetchBackend(target, args, fetchFn = fetch) {
|
|
|
339
406
|
...retryAfterHeader(res.headers),
|
|
340
407
|
}, "invalid_upstream_envelope");
|
|
341
408
|
}
|
|
342
|
-
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);
|
|
343
410
|
}
|
|
344
411
|
let body;
|
|
345
412
|
try {
|
|
@@ -356,7 +423,9 @@ export async function fetchBackend(target, args, fetchFn = fetch) {
|
|
|
356
423
|
...retryAfterHeader(res.headers),
|
|
357
424
|
}, "invalid_upstream_envelope");
|
|
358
425
|
}
|
|
359
|
-
|
|
426
|
+
const metadata = {};
|
|
427
|
+
captureReportedModel(metadata, body, "anthropic-messages", false);
|
|
428
|
+
return attachUpstreamMetadata(res, metadata);
|
|
360
429
|
}
|
|
361
430
|
// kind === "openai"
|
|
362
431
|
// llm-bridge stringifies any block type it doesn't know, which would put a document's
|
|
@@ -430,12 +499,16 @@ export async function fetchBackend(target, args, fetchFn = fetch) {
|
|
|
430
499
|
}
|
|
431
500
|
try {
|
|
432
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);
|
|
433
506
|
// Recover a tool call this host returned as raw dialect TEXT. Gated on the request actually
|
|
434
507
|
// declaring tools: with none declared there is no call to recover, and wrapping the stream
|
|
435
508
|
// would add holdback latency for nothing.
|
|
436
509
|
const schemas = toolSchemaMap(args.reqJson);
|
|
437
|
-
const body = schemas.size > 0 ? recoverDialectInStream(
|
|
438
|
-
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);
|
|
439
512
|
}
|
|
440
513
|
catch (e) {
|
|
441
514
|
return anthropicError(502, `llm-relay: response translation failed: ${e.message}`, "local", {}, "relay_mapper_defect");
|
|
@@ -476,13 +549,16 @@ export async function fetchBackend(target, args, fetchFn = fetch) {
|
|
|
476
549
|
// whose tool call the RELAY reconstructed is not the same as one that arrived correct, and an
|
|
477
550
|
// unflagged reconstruction is indistinguishable from a host that worked.
|
|
478
551
|
const recoveredDialect = recoveredDialectOf(anthropicJson);
|
|
479
|
-
|
|
552
|
+
const response = new Response(JSON.stringify(anthropicJson), {
|
|
480
553
|
status: 200,
|
|
481
554
|
headers: {
|
|
482
555
|
"content-type": "application/json",
|
|
483
|
-
...(recoveredDialect ? {
|
|
556
|
+
...(recoveredDialect ? { [TOOL_DIALECT_HEADER]: recoveredDialect } : {}),
|
|
484
557
|
},
|
|
485
558
|
});
|
|
559
|
+
const metadata = {};
|
|
560
|
+
captureReportedModel(metadata, upstreamJson, "openai-chat", false);
|
|
561
|
+
return attachUpstreamMetadata(response, metadata);
|
|
486
562
|
}
|
|
487
563
|
/** Did this translated message carry a tool call the relay reconstructed from text? */
|
|
488
564
|
function recoveredDialectOf(message) {
|
|
@@ -504,11 +580,14 @@ export function openAiResponseToAnthropic(j, model, schemas) {
|
|
|
504
580
|
const msg = choice.message ?? {};
|
|
505
581
|
const content = [];
|
|
506
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;
|
|
507
586
|
// Recover ONLY when the host parsed nothing. A host that populated `tool_calls` has already
|
|
508
587
|
// spoken; second-guessing it here would be inference, not translation.
|
|
509
588
|
let recovered = [];
|
|
510
|
-
if (toolCalls.length === 0 &&
|
|
511
|
-
const out = recoverToolCalls(
|
|
589
|
+
if (toolCalls.length === 0 && messageText !== null && messageText.length > 0) {
|
|
590
|
+
const out = recoverToolCalls(messageText, schemas ?? new Map());
|
|
512
591
|
if (out.status === "parsed") {
|
|
513
592
|
recovered = out.calls;
|
|
514
593
|
if (out.text.length > 0)
|
|
@@ -522,11 +601,11 @@ export function openAiResponseToAnthropic(j, model, schemas) {
|
|
|
522
601
|
throw new DialectUnparseableError(out.dialect);
|
|
523
602
|
}
|
|
524
603
|
else {
|
|
525
|
-
content.push({ type: "text", text:
|
|
604
|
+
content.push({ type: "text", text: messageText });
|
|
526
605
|
}
|
|
527
606
|
}
|
|
528
|
-
else if (
|
|
529
|
-
content.push({ type: "text", text:
|
|
607
|
+
else if (messageText !== null && messageText.length > 0) {
|
|
608
|
+
content.push({ type: "text", text: messageText });
|
|
530
609
|
}
|
|
531
610
|
if (recovered.length > 0) {
|
|
532
611
|
toolCalls = recovered.map((c, i) => ({
|
|
@@ -782,6 +861,7 @@ export async function fetchOpenAiFront(target, args, fetchFn = fetch) {
|
|
|
782
861
|
// Preserve the existing direct path for the protocol/backend pair that already speaks the
|
|
783
862
|
// same wire format. It keeps provider-specific OpenAI fields byte-for-byte intact.
|
|
784
863
|
if (target.kind === "openai" && protocol === "chat") {
|
|
864
|
+
const schemas = toolSchemaMap(base);
|
|
785
865
|
const body = { ...base, model: target.model, stream: args.wantsStream };
|
|
786
866
|
const res = await fetchFn(target.base + "/chat/completions", {
|
|
787
867
|
method: "POST",
|
|
@@ -800,7 +880,18 @@ export async function fetchOpenAiFront(target, args, fetchFn = fetch) {
|
|
|
800
880
|
if (!preflight.ok) {
|
|
801
881
|
return openaiError(502, `llm-relay: invalid OpenAI upstream envelope: ${preflight.reason}`, "upstream", "invalid_upstream_envelope", retryAfterHeader(res.headers));
|
|
802
882
|
}
|
|
803
|
-
|
|
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);
|
|
804
895
|
}
|
|
805
896
|
let responseBody;
|
|
806
897
|
try {
|
|
@@ -813,7 +904,28 @@ export async function fetchOpenAiFront(target, args, fetchFn = fetch) {
|
|
|
813
904
|
if (invalidReason) {
|
|
814
905
|
return openaiError(502, `llm-relay: invalid OpenAI upstream envelope: ${invalidReason}`, "upstream", "invalid_upstream_envelope", retryAfterHeader(res.headers));
|
|
815
906
|
}
|
|
816
|
-
|
|
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);
|
|
817
929
|
}
|
|
818
930
|
let anthropicBody;
|
|
819
931
|
try {
|
|
@@ -836,6 +948,7 @@ export async function fetchOpenAiFront(target, args, fetchFn = fetch) {
|
|
|
836
948
|
wantsStream: args.wantsStream,
|
|
837
949
|
signal: args.signal,
|
|
838
950
|
}, fetchFn);
|
|
951
|
+
const metadata = upstreamResponseMetadata.get(backendRes);
|
|
839
952
|
if (!backendRes.ok) {
|
|
840
953
|
const raw = await backendRes.text().catch(() => "");
|
|
841
954
|
const origin = errorOrigin(backendRes) ?? "upstream";
|
|
@@ -862,7 +975,8 @@ export async function fetchOpenAiFront(target, args, fetchFn = fetch) {
|
|
|
862
975
|
const targetProtocol = protocol === "responses" ? "openai-responses" : "openai";
|
|
863
976
|
try {
|
|
864
977
|
const output = handleUniversalStreamRequest(preflight.body, "anthropic", targetProtocol);
|
|
865
|
-
|
|
978
|
+
const response = new Response(output, { status: backendRes.status, headers: { "content-type": "text/event-stream" } });
|
|
979
|
+
return metadata ? attachUpstreamMetadata(response, metadata) : response;
|
|
866
980
|
}
|
|
867
981
|
catch (e) {
|
|
868
982
|
return openaiError(502, `llm-relay: response translation failed: ${e.message}`, "local", "relay_mapper_defect");
|
|
@@ -884,10 +998,11 @@ export async function fetchOpenAiFront(target, args, fetchFn = fetch) {
|
|
|
884
998
|
: `llm-relay: invalid Anthropic upstream envelope: ${invalidReason}`, generated ? "local" : "upstream", generated ? "relay_mapper_defect" : "invalid_upstream_envelope", retryAfterHeader(backendRes.headers));
|
|
885
999
|
}
|
|
886
1000
|
try {
|
|
887
|
-
|
|
1001
|
+
const response = new Response(JSON.stringify(anthropicMessageToOpenAi(body, protocol, target.model ?? String(base.model ?? ""))), {
|
|
888
1002
|
status: backendRes.status,
|
|
889
1003
|
headers: { "content-type": "application/json" },
|
|
890
1004
|
});
|
|
1005
|
+
return metadata ? attachUpstreamMetadata(response, metadata) : response;
|
|
891
1006
|
}
|
|
892
1007
|
catch (e) {
|
|
893
1008
|
return openaiError(502, `llm-relay: response translation failed: ${e.message}`, "local", "relay_mapper_defect");
|