@koda-sl/baker-bridge 0.56.1 → 0.59.0
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 +17 -7
- package/dist/hono/env.d.ts +1 -0
- package/dist/hono/env.d.ts.map +1 -1
- package/dist/hono/env.js +5 -0
- package/dist/hono/env.js.map +1 -1
- package/dist/hono/model-router.d.ts +2 -1
- package/dist/hono/model-router.d.ts.map +1 -1
- package/dist/hono/model-router.js +24 -5
- package/dist/hono/model-router.js.map +1 -1
- package/dist/hono/model-router.test.js +64 -12
- package/dist/hono/model-router.test.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -45,8 +45,9 @@ await server.start();
|
|
|
45
45
|
| `ANTHROPIC_BASE_URL` / `ANTHROPIC_AUTH_TOKEN` | OpenRouter routing (base URL + per-org key) injected into the sandbox. Forwarded to the Agent SDK subprocess so the agent can reach its model |
|
|
46
46
|
| `BAKER_CHAT_ID` | Present only when running inside a chat sandbox. When set, the bridge relocates Claude Code auto-memory into the cloned repo (`.claude/memory`) — see [Session journal](#session-journal-agent-memory) |
|
|
47
47
|
| `BAKER_ACTING_USER_ID` | Injected into the Agent SDK subprocess env per turn (when the turn's sender is known) so in-sandbox `baker` CLI calls can scope work to the acting user — e.g. `baker mcp add --scope user` |
|
|
48
|
-
| `NOTDIAMOND_ENABLED` | `"true"` to route each prompt through NotDiamond's model selector. Defaults to `false` — every turn runs on
|
|
48
|
+
| `NOTDIAMOND_ENABLED` | `"true"` to route each prompt through NotDiamond's model selector. Defaults to `false` — every turn runs on the default model (`claude-sonnet-5`). Injected into the sandbox from the backend's own `NOTDIAMOND_ENABLED`, so flipping it is a Convex env change, not a deploy |
|
|
49
49
|
| `NOTDIAMOND_API_KEY` | NotDiamond bearer token. Required when `NOTDIAMOND_ENABLED=true` |
|
|
50
|
+
| `NOTDIAMOND_TRADEOFF` | What the router optimizes for: `cost` (cheaper model), `latency` (faster model), or `quality` (NotDiamond's own default — no tradeoff is sent). Defaults to `cost`. Also injected from the backend env of the same name |
|
|
50
51
|
|
|
51
52
|
All variables are validated at startup via `@t3-oss/env-core` + `zod`.
|
|
52
53
|
|
|
@@ -54,14 +55,23 @@ All variables are validated at startup via `@t3-oss/env-core` + `zod`.
|
|
|
54
55
|
|
|
55
56
|
The bridge picks a Claude model per turn before calling the Agent SDK:
|
|
56
57
|
|
|
57
|
-
- **Default (router disabled):** every turn runs on `claude-
|
|
58
|
+
- **Default (router disabled):** every turn runs on `claude-sonnet-5` — ~60% cheaper per token than an Opus tier, at near-Opus quality on agentic work.
|
|
58
59
|
- **Router enabled (`NOTDIAMOND_ENABLED=true`):** the prompt + repo context are
|
|
59
|
-
sent to NotDiamond with three candidates (Haiku 4.5, Sonnet
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
sent to NotDiamond with three candidates (Haiku 4.5, Sonnet 5, Opus 4.6).
|
|
61
|
+
The request carries a `tradeoff` (`NOTDIAMOND_TRADEOFF`, default `cost`), so
|
|
62
|
+
the router picks the cheaper model whenever quality wouldn't suffer; set it to
|
|
63
|
+
`latency` to optimize for speed instead, or `quality` to restore NotDiamond's
|
|
64
|
+
own default (which sends no `tradeoff` at all — `quality` is a bridge-side
|
|
65
|
+
setting, not a wire value). The field is only supported by NotDiamond's
|
|
66
|
+
default pre-trained router — a custom trained router rejects it with a 422,
|
|
67
|
+
which the bridge treats as a router error and falls back to the default model.
|
|
68
|
+
The returned OpenRouter-formatted ID is normalized to the
|
|
62
69
|
Anthropic-native form (e.g. `anthropic/claude-opus-4.6` → `claude-opus-4-6`)
|
|
63
|
-
before being passed to `query({ options: { model } })`.
|
|
64
|
-
|
|
70
|
+
before being passed to `query({ options: { model } })`. A candidate NotDiamond
|
|
71
|
+
doesn't list is dropped from the candidate set silently rather than rejected,
|
|
72
|
+
so it looks routed but is never selected — check any new candidate against
|
|
73
|
+
`GET https://api.notdiamond.ai/v2/models?provider=anthropic` first. On any
|
|
74
|
+
router error or empty response, it falls back to `claude-sonnet-5`.
|
|
65
75
|
|
|
66
76
|
## Endpoints
|
|
67
77
|
|
package/dist/hono/env.d.ts
CHANGED
package/dist/hono/env.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../src/hono/env.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,GAAG
|
|
1
|
+
{"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../src/hono/env.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,GAAG;;;;;;;;;EAmBd,CAAC"}
|
package/dist/hono/env.js
CHANGED
|
@@ -12,6 +12,11 @@ export const env = createEnv({
|
|
|
12
12
|
.default("false")
|
|
13
13
|
.transform((v) => v === "true"),
|
|
14
14
|
NOTDIAMOND_API_KEY: z.string().min(1).optional(),
|
|
15
|
+
// What the router optimizes for: "cost" prefers the cheaper model, "latency"
|
|
16
|
+
// the faster one. "quality" is NotDiamond's default and has no wire value —
|
|
17
|
+
// we send no `tradeoff` field at all for it. `tradeoff` is only valid on the
|
|
18
|
+
// default pre-trained router; custom trained routers reject it with a 422.
|
|
19
|
+
NOTDIAMOND_TRADEOFF: z.enum(["cost", "latency", "quality"]).default("cost"),
|
|
15
20
|
},
|
|
16
21
|
runtimeEnv: process.env,
|
|
17
22
|
});
|
package/dist/hono/env.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"env.js","sourceRoot":"","sources":["../../src/hono/env.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,CAAC,MAAM,GAAG,GAAG,SAAS,CAAC;IAC3B,MAAM,EAAE;QACN,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;QAC7B,qBAAqB,EAAE,CAAC,CAAC,GAAG,EAAE;QAC9B,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;QAChC,iBAAiB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC;QACpD,mBAAmB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;QACtC,kBAAkB,EAAE,CAAC;aAClB,MAAM,EAAE;aACR,OAAO,CAAC,OAAO,CAAC;aAChB,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,MAAM,CAAC;QACjC,kBAAkB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;
|
|
1
|
+
{"version":3,"file":"env.js","sourceRoot":"","sources":["../../src/hono/env.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,CAAC,MAAM,GAAG,GAAG,SAAS,CAAC;IAC3B,MAAM,EAAE;QACN,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;QAC7B,qBAAqB,EAAE,CAAC,CAAC,GAAG,EAAE;QAC9B,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;QAChC,iBAAiB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC;QACpD,mBAAmB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;QACtC,kBAAkB,EAAE,CAAC;aAClB,MAAM,EAAE;aACR,OAAO,CAAC,OAAO,CAAC;aAChB,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,MAAM,CAAC;QACjC,kBAAkB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;QAChD,6EAA6E;QAC7E,4EAA4E;QAC5E,6EAA6E;QAC7E,2EAA2E;QAC3E,mBAAmB,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC;KAC5E;IACD,UAAU,EAAE,OAAO,CAAC,GAAG;CACxB,CAAC,CAAC"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export declare const CANDIDATE_MODELS: {
|
|
2
2
|
model: string;
|
|
3
3
|
}[];
|
|
4
|
-
export declare const DEFAULT_MODEL = "claude-
|
|
4
|
+
export declare const DEFAULT_MODEL = "claude-sonnet-5";
|
|
5
5
|
export declare function normalizeModel(model: string): string;
|
|
6
6
|
export declare function contextWindowForModel(model: string | undefined | null): number | null;
|
|
7
7
|
interface SelectModelResult {
|
|
@@ -14,6 +14,7 @@ interface SelectModelResult {
|
|
|
14
14
|
/**
|
|
15
15
|
* Use NotDiamond to select the optimal Claude model for a given prompt.
|
|
16
16
|
* Set NOTDIAMOND_ENABLED=true to enable routing; defaults to Opus 4.6 otherwise.
|
|
17
|
+
* NOTDIAMOND_TRADEOFF decides what it optimizes for (see its env.ts definition).
|
|
17
18
|
*/
|
|
18
19
|
export declare function selectModel(prompt: string): Promise<SelectModelResult>;
|
|
19
20
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model-router.d.ts","sourceRoot":"","sources":["../../src/hono/model-router.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"model-router.d.ts","sourceRoot":"","sources":["../../src/hono/model-router.ts"],"names":[],"mappings":"AAcA,eAAO,MAAM,gBAAgB;;GAI5B,CAAC;AAQF,eAAO,MAAM,aAAa,oBAAoB,CAAC;AAE/C,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAcpD;AAoBD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAKrF;AAYD,UAAU,iBAAiB;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACpD;AAED;;;;GAIG;AACH,wBAAsB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CA+C5E"}
|
|
@@ -2,13 +2,26 @@ import { env } from "./env.js";
|
|
|
2
2
|
import { buildRepoContext } from "./repo-context.js";
|
|
3
3
|
const NOTDIAMOND_API_URL = "https://api.notdiamond.ai/v2/modelRouter/modelSelect";
|
|
4
4
|
// OpenRouter-formatted IDs sent to NotDiamond's selection API.
|
|
5
|
+
//
|
|
6
|
+
// Only models NotDiamond actually lists belong here — it silently DROPS an
|
|
7
|
+
// unrecognized entry from the candidate set rather than erroring, so an
|
|
8
|
+
// unsupported model looks routed but can never be selected. Check a new
|
|
9
|
+
// candidate against `GET https://api.notdiamond.ai/v2/models?provider=anthropic`
|
|
10
|
+
// (match on its `openrouter_model`) before adding it — that catalog currently
|
|
11
|
+
// tops out at Sonnet 5 / Opus 4.7, so newer ids are not routable.
|
|
12
|
+
// Keep in sync with MODEL_CONTEXT_WINDOW.
|
|
5
13
|
export const CANDIDATE_MODELS = [
|
|
6
14
|
{ model: "anthropic/claude-haiku-4.5" },
|
|
7
|
-
{ model: "anthropic/claude-sonnet-
|
|
15
|
+
{ model: "anthropic/claude-sonnet-5" },
|
|
8
16
|
{ model: "anthropic/claude-opus-4.6" },
|
|
9
17
|
];
|
|
10
18
|
// Anthropic-native ID expected by the Claude Agent SDK `query({ options: { model } })`.
|
|
11
|
-
|
|
19
|
+
// Sonnet 5 rather than Opus: $2/$10 per MTok on OpenRouter against Opus 4.6's
|
|
20
|
+
// $5/$25, so every un-routed turn costs ~60% less, and it holds near-Opus
|
|
21
|
+
// quality on the agentic/coding work this runtime does. Sonnet 5's tokenizer
|
|
22
|
+
// emits ~30% more tokens for the same text, which erodes but does not come
|
|
23
|
+
// close to erasing that gap.
|
|
24
|
+
export const DEFAULT_MODEL = "claude-sonnet-5";
|
|
12
25
|
export function normalizeModel(model) {
|
|
13
26
|
const lower = model.toLowerCase();
|
|
14
27
|
if (lower.includes("opus")) {
|
|
@@ -17,8 +30,10 @@ export function normalizeModel(model) {
|
|
|
17
30
|
if (lower.includes("haiku")) {
|
|
18
31
|
return "claude-haiku-4-5";
|
|
19
32
|
}
|
|
33
|
+
// Sonnet 5 dropped the 4.x version prefix, so the family keyword alone can no
|
|
34
|
+
// longer pick the id — anything still on a 4.x slug maps to 4.6, the rest to 5.
|
|
20
35
|
if (lower.includes("sonnet")) {
|
|
21
|
-
return "claude-sonnet-4-6";
|
|
36
|
+
return lower.includes("sonnet-4") ? "claude-sonnet-4-6" : "claude-sonnet-5";
|
|
22
37
|
}
|
|
23
38
|
return model;
|
|
24
39
|
}
|
|
@@ -27,10 +42,12 @@ export function normalizeModel(model) {
|
|
|
27
42
|
// models through the OpenRouter passthrough (`ANTHROPIC_BASE_URL=openrouter.ai`),
|
|
28
43
|
// and the Claude Code CLI's reported `contextWindow` is its own catalog value for
|
|
29
44
|
// the Anthropic-native id — the standard 200k for Claude — which does NOT reflect
|
|
30
|
-
// the OpenRouter window (opus/sonnet
|
|
31
|
-
// normalized id we pass to the SDK. Keep in sync with CANDIDATE_MODELS
|
|
45
|
+
// the OpenRouter window (opus/sonnet are served at 1M there). Keyed by the
|
|
46
|
+
// normalized id we pass to the SDK. Keep in sync with CANDIDATE_MODELS; Sonnet
|
|
47
|
+
// 4.6 stays listed so a turn already running on it still resolves.
|
|
32
48
|
const MODEL_CONTEXT_WINDOW = {
|
|
33
49
|
"claude-opus-4-6": 1_000_000,
|
|
50
|
+
"claude-sonnet-5": 1_000_000,
|
|
34
51
|
"claude-sonnet-4-6": 1_000_000,
|
|
35
52
|
"claude-haiku-4-5": 200_000,
|
|
36
53
|
};
|
|
@@ -54,6 +71,7 @@ function getRepoContext() {
|
|
|
54
71
|
/**
|
|
55
72
|
* Use NotDiamond to select the optimal Claude model for a given prompt.
|
|
56
73
|
* Set NOTDIAMOND_ENABLED=true to enable routing; defaults to Opus 4.6 otherwise.
|
|
74
|
+
* NOTDIAMOND_TRADEOFF decides what it optimizes for (see its env.ts definition).
|
|
57
75
|
*/
|
|
58
76
|
export async function selectModel(prompt) {
|
|
59
77
|
const systemPrompt = await getRepoContext();
|
|
@@ -74,6 +92,7 @@ export async function selectModel(prompt) {
|
|
|
74
92
|
body: JSON.stringify({
|
|
75
93
|
messages,
|
|
76
94
|
llm_providers: CANDIDATE_MODELS,
|
|
95
|
+
...(env.NOTDIAMOND_TRADEOFF === "quality" ? {} : { tradeoff: env.NOTDIAMOND_TRADEOFF }),
|
|
77
96
|
}),
|
|
78
97
|
});
|
|
79
98
|
if (!response.ok) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model-router.js","sourceRoot":"","sources":["../../src/hono/model-router.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAC/B,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAErD,MAAM,kBAAkB,GAAG,sDAAsD,CAAC;AAElF,+DAA+D;AAC/D,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B,EAAE,KAAK,EAAE,4BAA4B,EAAE;IACvC,EAAE,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"model-router.js","sourceRoot":"","sources":["../../src/hono/model-router.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from "vitest";
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
// Stub env before importing model-router so env validation passes.
|
|
3
|
-
vi.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
const mockEnv = vi.hoisted(() => ({
|
|
4
|
+
BAKER_CONVEX_SITE_URL: "https://example.convex.site",
|
|
5
|
+
BAKER_API_KEY: "bk_test",
|
|
6
|
+
AUTH_TOKEN: "tok",
|
|
7
|
+
INTERNAL_SECRET_KEY: "0".repeat(64),
|
|
8
|
+
NOTDIAMOND_ENABLED: false,
|
|
9
|
+
NOTDIAMOND_API_KEY: "nd_test",
|
|
10
|
+
NOTDIAMOND_TRADEOFF: "cost",
|
|
11
11
|
}));
|
|
12
|
-
|
|
12
|
+
vi.mock("./env.ts", () => ({ env: mockEnv }));
|
|
13
|
+
vi.mock("./repo-context.ts", () => ({ buildRepoContext: async () => "repo context" }));
|
|
14
|
+
import { CANDIDATE_MODELS, contextWindowForModel, DEFAULT_MODEL, normalizeModel, selectModel } from "./model-router.js";
|
|
13
15
|
// The Claude Agent SDK's `query({ options: { model } })` expects native
|
|
14
16
|
// Anthropic IDs (no `anthropic/` prefix, dashes-only versions). Passing
|
|
15
17
|
// NotDiamond / OpenRouter formatted IDs makes the SDK silently fall back to
|
|
@@ -21,6 +23,15 @@ describe("normalizeModel", () => {
|
|
|
21
23
|
it("normalizes Sonnet (already dashed) by stripping the prefix", () => {
|
|
22
24
|
expect(normalizeModel("anthropic/claude-sonnet-4-6")).toBe("claude-sonnet-4-6");
|
|
23
25
|
});
|
|
26
|
+
// Sonnet 5 has no 4.x version prefix, so matching on "sonnet" alone would
|
|
27
|
+
// silently route it to the older 4.6 id and mis-report its context window.
|
|
28
|
+
it("normalizes Sonnet 5 without collapsing it into the 4.6 id", () => {
|
|
29
|
+
expect(normalizeModel("anthropic/claude-sonnet-5")).toBe("claude-sonnet-5");
|
|
30
|
+
});
|
|
31
|
+
it("still maps dotted and dated 4.x Sonnet slugs to 4.6", () => {
|
|
32
|
+
expect(normalizeModel("anthropic/claude-sonnet-4.6")).toBe("claude-sonnet-4-6");
|
|
33
|
+
expect(normalizeModel("anthropic/claude-sonnet-4-5-20250929")).toBe("claude-sonnet-4-6");
|
|
34
|
+
});
|
|
24
35
|
it("normalizes Haiku by stripping the prefix", () => {
|
|
25
36
|
expect(normalizeModel("anthropic/claude-haiku-4-5")).toBe("claude-haiku-4-5");
|
|
26
37
|
});
|
|
@@ -44,6 +55,9 @@ describe("contextWindowForModel", () => {
|
|
|
44
55
|
it("returns 1M for Sonnet 4.6", () => {
|
|
45
56
|
expect(contextWindowForModel("claude-sonnet-4-6")).toBe(1_000_000);
|
|
46
57
|
});
|
|
58
|
+
it("returns 1M for Sonnet 5", () => {
|
|
59
|
+
expect(contextWindowForModel("claude-sonnet-5")).toBe(1_000_000);
|
|
60
|
+
});
|
|
47
61
|
it("returns 200k for Haiku 4.5 (not a 1M model on OpenRouter)", () => {
|
|
48
62
|
expect(contextWindowForModel("claude-haiku-4-5")).toBe(200_000);
|
|
49
63
|
});
|
|
@@ -66,11 +80,49 @@ describe("contextWindowForModel", () => {
|
|
|
66
80
|
}
|
|
67
81
|
});
|
|
68
82
|
});
|
|
83
|
+
describe("selectModel", () => {
|
|
84
|
+
afterEach(() => {
|
|
85
|
+
mockEnv.NOTDIAMOND_ENABLED = false;
|
|
86
|
+
mockEnv.NOTDIAMOND_TRADEOFF = "cost";
|
|
87
|
+
vi.unstubAllGlobals();
|
|
88
|
+
});
|
|
89
|
+
async function capturePostedBody() {
|
|
90
|
+
const fetchMock = vi.fn(async (_url, _init) => new Response(JSON.stringify({ providers: [{ model: "anthropic/claude-haiku-4-5" }] })));
|
|
91
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
92
|
+
await selectModel("write a landing page");
|
|
93
|
+
const [, init] = fetchMock.mock.calls[0] ?? [];
|
|
94
|
+
return JSON.parse(String(init?.body));
|
|
95
|
+
}
|
|
96
|
+
// Without a tradeoff NotDiamond optimizes for quality alone, which pins nearly
|
|
97
|
+
// every turn to Opus. Sending it is what makes the router actually save money.
|
|
98
|
+
it("sends the configured tradeoff so the router optimizes for cost by default", async () => {
|
|
99
|
+
mockEnv.NOTDIAMOND_ENABLED = true;
|
|
100
|
+
expect(await capturePostedBody()).toMatchObject({ tradeoff: "cost" });
|
|
101
|
+
});
|
|
102
|
+
it("sends latency when configured to optimize for speed", async () => {
|
|
103
|
+
mockEnv.NOTDIAMOND_ENABLED = true;
|
|
104
|
+
mockEnv.NOTDIAMOND_TRADEOFF = "latency";
|
|
105
|
+
expect(await capturePostedBody()).toMatchObject({ tradeoff: "latency" });
|
|
106
|
+
});
|
|
107
|
+
// "quality" is the router's default posture, not a wire enum — sending it
|
|
108
|
+
// literally would be rejected, so the field has to be dropped instead.
|
|
109
|
+
it("omits the tradeoff field entirely when configured for quality", async () => {
|
|
110
|
+
mockEnv.NOTDIAMOND_ENABLED = true;
|
|
111
|
+
mockEnv.NOTDIAMOND_TRADEOFF = "quality";
|
|
112
|
+
expect(await capturePostedBody()).not.toHaveProperty("tradeoff");
|
|
113
|
+
});
|
|
114
|
+
});
|
|
69
115
|
describe("DEFAULT_MODEL", () => {
|
|
116
|
+
// Every turn that isn't routed runs on this model, so it is the single
|
|
117
|
+
// biggest lever on the runtime's model bill. Opus 4.6 costs $5/$25 per MTok
|
|
118
|
+
// against Sonnet 5's $2/$10 — pin the cheaper default so a silent revert to
|
|
119
|
+
// Opus shows up here rather than on the invoice.
|
|
120
|
+
it("defaults to Sonnet 5 rather than an Opus tier", () => {
|
|
121
|
+
expect(DEFAULT_MODEL).toBe("claude-sonnet-5");
|
|
122
|
+
});
|
|
70
123
|
// Locks the fallback to the SDK-native format. If this regresses to the
|
|
71
|
-
// OpenRouter/NotDiamond form, the SDK silently
|
|
124
|
+
// OpenRouter/NotDiamond form, the SDK silently falls back to its CLI default.
|
|
72
125
|
it("uses the Claude Agent SDK-native format", () => {
|
|
73
|
-
expect(DEFAULT_MODEL).toBe("claude-opus-4-6");
|
|
74
126
|
expect(DEFAULT_MODEL).not.toMatch(/^anthropic\//);
|
|
75
127
|
expect(DEFAULT_MODEL).not.toMatch(/\./);
|
|
76
128
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model-router.test.js","sourceRoot":"","sources":["../../src/hono/model-router.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"model-router.test.js","sourceRoot":"","sources":["../../src/hono/model-router.test.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED