auto-model-router 0.1.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/.env.example +24 -0
- package/.github/workflows/publish.yml +40 -0
- package/.omp-plugin/marketplace.json +30 -0
- package/LICENSE +21 -0
- package/README.md +639 -0
- package/bun.lock +32 -0
- package/docs/claude-anthropic-wire.md +116 -0
- package/omp-extension/configure-logic.ts +128 -0
- package/omp-extension/embed-logic.ts +141 -0
- package/omp-extension/router-configure.ts +111 -0
- package/omp-extension/router-embed.ts +118 -0
- package/omp-extension/router-toast.ts +130 -0
- package/omp-extension/toast-logic.ts +136 -0
- package/package.json +56 -0
- package/src/catalog/openrouter-catalog.ts +428 -0
- package/src/catalog/types.ts +104 -0
- package/src/cli/args.ts +105 -0
- package/src/cli/config-cmd.ts +362 -0
- package/src/cli/config-wizard.ts +636 -0
- package/src/cli/explain.ts +167 -0
- package/src/cli/models.ts +240 -0
- package/src/cli/stats.ts +69 -0
- package/src/config/defaults.ts +136 -0
- package/src/config/load.ts +143 -0
- package/src/config/omp-credentials.ts +124 -0
- package/src/config/schema.ts +161 -0
- package/src/config/types.ts +244 -0
- package/src/cost/blended.ts +80 -0
- package/src/cost/forecast.ts +129 -0
- package/src/cost/ledger.ts +291 -0
- package/src/cost/types.ts +148 -0
- package/src/index.ts +93 -0
- package/src/router/cache-control.ts +66 -0
- package/src/router/candidates.ts +246 -0
- package/src/router/classify.ts +329 -0
- package/src/router/escalate.ts +264 -0
- package/src/router/features.ts +225 -0
- package/src/router/index.ts +99 -0
- package/src/router/select.ts +365 -0
- package/src/router/state.ts +118 -0
- package/src/router/tier-plan.ts +151 -0
- package/src/router/types.ts +222 -0
- package/src/server/http.ts +343 -0
- package/src/server/turn.ts +393 -0
- package/src/tokens/estimate.ts +74 -0
- package/src/upstream/openrouter.ts +221 -0
- package/src/upstream/sse-parse.ts +208 -0
- package/src/upstream/types.ts +75 -0
- package/src/util/hash.ts +0 -0
- package/src/util/log.ts +53 -0
- package/src/util/sqlite.ts +140 -0
- package/src/util/sse.ts +23 -0
- package/src/wire/openai/errors.ts +48 -0
- package/src/wire/openai/models.ts +37 -0
- package/src/wire/openai/request.ts +279 -0
- package/src/wire/openai/sink.ts +213 -0
- package/src/wire/types.ts +156 -0
- package/test/catalog.test.ts +319 -0
- package/test/classify.test.ts +269 -0
- package/test/config-wizard.test.ts +482 -0
- package/test/config.test.ts +121 -0
- package/test/configure-logic.test.ts +151 -0
- package/test/cost.test.ts +137 -0
- package/test/embed-logic.test.ts +107 -0
- package/test/escalate.test.ts +223 -0
- package/test/failover.test.ts +494 -0
- package/test/features.test.ts +228 -0
- package/test/fixtures/openrouter-models.json +15340 -0
- package/test/models-yml.test.ts +186 -0
- package/test/omp-credentials.test.ts +185 -0
- package/test/select.test.ts +538 -0
- package/test/sse-parse.test.ts +142 -0
- package/test/tier-plan.test.ts +302 -0
- package/test/toast-logic.test.ts +160 -0
- package/test/tokens.test.ts +160 -0
- package/test/trust-attribution.test.ts +175 -0
- package/test/turn.test.ts +498 -0
- package/test/wire-request.test.ts +297 -0
- package/test/wire-sink.test.ts +179 -0
- package/tools/install.ts +140 -0
- package/tools/mock-openrouter.ts +269 -0
- package/tools/smoke.ts +326 -0
- package/tsconfig.json +23 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { extractFeatures } from "../src/router/features.ts";
|
|
4
|
+
import { parseChatRequest } from "../src/wire/openai/request.ts";
|
|
5
|
+
import type { NormRequest } from "../src/wire/types.ts";
|
|
6
|
+
|
|
7
|
+
const TOOLS = [
|
|
8
|
+
{
|
|
9
|
+
type: "function",
|
|
10
|
+
function: {
|
|
11
|
+
name: "bash",
|
|
12
|
+
description: "Run a shell command",
|
|
13
|
+
parameters: { type: "object", properties: { command: { type: "string" } } },
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
type: "function",
|
|
18
|
+
function: {
|
|
19
|
+
name: "read",
|
|
20
|
+
description: "Read a file",
|
|
21
|
+
parameters: { type: "object", properties: { path: { type: "string" } } },
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
function req(messages: unknown[], tools = TOOLS): NormRequest {
|
|
27
|
+
return parseChatRequest({ model: "auto", messages, tools }, new Headers());
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function toolCall(id: string, name: string, args: string) {
|
|
31
|
+
return { role: "assistant", content: null, tool_calls: [{ id, type: "function", function: { name, arguments: args } }] };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const SYSTEM = { role: "system", content: "You are a coding agent." };
|
|
35
|
+
|
|
36
|
+
describe("agent-loop shape", () => {
|
|
37
|
+
test("a tool-result tail is a mechanical continuation, not fresh intent", () => {
|
|
38
|
+
const f = extractFeatures(
|
|
39
|
+
req([
|
|
40
|
+
SYSTEM,
|
|
41
|
+
{ role: "user", content: "check the version" },
|
|
42
|
+
toolCall("c1", "read", '{"path":"package.json"}'),
|
|
43
|
+
{ role: "tool", tool_call_id: "c1", name: "read", content: '{"version":"1.0.0"}' },
|
|
44
|
+
]),
|
|
45
|
+
100,
|
|
46
|
+
);
|
|
47
|
+
expect(f.isToolResultContinuation).toBe(true);
|
|
48
|
+
expect(f.toolLoopDepth).toBeGreaterThan(0);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("a trailing user message is fresh intent, not a continuation", () => {
|
|
52
|
+
const f = extractFeatures(req([SYSTEM, { role: "user", content: "add a retry" }]), 100);
|
|
53
|
+
expect(f.isToolResultContinuation).toBe(false);
|
|
54
|
+
expect(f.toolLoopDepth).toBe(0);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("loop depth grows with consecutive tool round-trips", () => {
|
|
58
|
+
const shallow = extractFeatures(
|
|
59
|
+
req([SYSTEM, { role: "user", content: "go" }, toolCall("c1", "read", "{}"), { role: "tool", tool_call_id: "c1", content: "a" }]),
|
|
60
|
+
100,
|
|
61
|
+
);
|
|
62
|
+
const deep = extractFeatures(
|
|
63
|
+
req([
|
|
64
|
+
SYSTEM,
|
|
65
|
+
{ role: "user", content: "go" },
|
|
66
|
+
toolCall("c1", "read", '{"path":"a"}'),
|
|
67
|
+
{ role: "tool", tool_call_id: "c1", content: "a" },
|
|
68
|
+
toolCall("c2", "read", '{"path":"b"}'),
|
|
69
|
+
{ role: "tool", tool_call_id: "c2", content: "b" },
|
|
70
|
+
toolCall("c3", "read", '{"path":"c"}'),
|
|
71
|
+
{ role: "tool", tool_call_id: "c3", content: "c" },
|
|
72
|
+
]),
|
|
73
|
+
100,
|
|
74
|
+
);
|
|
75
|
+
expect(deep.toolLoopDepth).toBeGreaterThan(shallow.toolLoopDepth);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("counts distinct tools actually used, not merely offered", () => {
|
|
79
|
+
const f = extractFeatures(
|
|
80
|
+
req([
|
|
81
|
+
SYSTEM,
|
|
82
|
+
{ role: "user", content: "go" },
|
|
83
|
+
toolCall("c1", "read", '{"path":"a"}'),
|
|
84
|
+
{ role: "tool", tool_call_id: "c1", content: "a" },
|
|
85
|
+
toolCall("c2", "bash", '{"command":"ls"}'),
|
|
86
|
+
{ role: "tool", tool_call_id: "c2", content: "a.txt" },
|
|
87
|
+
]),
|
|
88
|
+
100,
|
|
89
|
+
);
|
|
90
|
+
expect(f.toolCount).toBe(2);
|
|
91
|
+
expect(f.distinctToolsUsed).toBe(2);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("failure and loop signals", () => {
|
|
96
|
+
test("detects a failing tool result", () => {
|
|
97
|
+
const f = extractFeatures(
|
|
98
|
+
req([
|
|
99
|
+
SYSTEM,
|
|
100
|
+
{ role: "user", content: "build it" },
|
|
101
|
+
toolCall("c1", "bash", '{"command":"make"}'),
|
|
102
|
+
{ role: "tool", tool_call_id: "c1", content: "make: *** [all] Error 2\ncommand not found: cc" },
|
|
103
|
+
]),
|
|
104
|
+
100,
|
|
105
|
+
);
|
|
106
|
+
expect(f.lastToolFailed).toBe(true);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("does not cry failure over ordinary output", () => {
|
|
110
|
+
// A false positive here escalates to an expensive model for nothing.
|
|
111
|
+
const f = extractFeatures(
|
|
112
|
+
req([
|
|
113
|
+
SYSTEM,
|
|
114
|
+
{ role: "user", content: "list files" },
|
|
115
|
+
toolCall("c1", "bash", '{"command":"ls"}'),
|
|
116
|
+
{ role: "tool", tool_call_id: "c1", content: "README.md\nsrc\npackage.json" },
|
|
117
|
+
]),
|
|
118
|
+
100,
|
|
119
|
+
);
|
|
120
|
+
expect(f.lastToolFailed).toBe(false);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("detects an identical repeated tool call as a loop", () => {
|
|
124
|
+
const f = extractFeatures(
|
|
125
|
+
req([
|
|
126
|
+
SYSTEM,
|
|
127
|
+
{ role: "user", content: "go" },
|
|
128
|
+
toolCall("c1", "read", '{"path":"same.ts"}'),
|
|
129
|
+
{ role: "tool", tool_call_id: "c1", content: "x" },
|
|
130
|
+
toolCall("c2", "read", '{"path":"same.ts"}'),
|
|
131
|
+
{ role: "tool", tool_call_id: "c2", content: "x" },
|
|
132
|
+
]),
|
|
133
|
+
100,
|
|
134
|
+
);
|
|
135
|
+
expect(f.repeatedToolCall).toBe(true);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("different arguments to the same tool are not a loop", () => {
|
|
139
|
+
const f = extractFeatures(
|
|
140
|
+
req([
|
|
141
|
+
SYSTEM,
|
|
142
|
+
{ role: "user", content: "go" },
|
|
143
|
+
toolCall("c1", "read", '{"path":"a.ts"}'),
|
|
144
|
+
{ role: "tool", tool_call_id: "c1", content: "x" },
|
|
145
|
+
toolCall("c2", "read", '{"path":"b.ts"}'),
|
|
146
|
+
{ role: "tool", tool_call_id: "c2", content: "y" },
|
|
147
|
+
]),
|
|
148
|
+
100,
|
|
149
|
+
);
|
|
150
|
+
expect(f.repeatedToolCall).toBe(false);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe("newest-content scoping", () => {
|
|
155
|
+
test("keywords are read from the newest user content only, not from history", () => {
|
|
156
|
+
// History mentioning "architecture" must not permanently inflate every
|
|
157
|
+
// later turn in a long session.
|
|
158
|
+
const f = extractFeatures(
|
|
159
|
+
req([
|
|
160
|
+
SYSTEM,
|
|
161
|
+
{ role: "user", content: "Explain the architecture and the deadlock root cause." },
|
|
162
|
+
{ role: "assistant", content: "Here is the design." },
|
|
163
|
+
{ role: "user", content: "fix the typo" },
|
|
164
|
+
]),
|
|
165
|
+
100,
|
|
166
|
+
);
|
|
167
|
+
expect(f.complexityKeywords).toHaveLength(0);
|
|
168
|
+
expect(f.trivialityKeywords.length).toBeGreaterThan(0);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("picks up complexity keywords when they are actually current", () => {
|
|
172
|
+
const f = extractFeatures(
|
|
173
|
+
req([SYSTEM, { role: "user", content: "Find the root cause of this race condition and redesign the architecture." }]),
|
|
174
|
+
100,
|
|
175
|
+
);
|
|
176
|
+
expect(f.complexityKeywords.length).toBeGreaterThan(0);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("measures code volume in the newest content", () => {
|
|
180
|
+
const f = extractFeatures(
|
|
181
|
+
req([SYSTEM, { role: "user", content: "review this\n```ts\nconst a = 1;\nconst b = 2;\n```" }]),
|
|
182
|
+
100,
|
|
183
|
+
);
|
|
184
|
+
expect(f.codeBlocks).toBe(1);
|
|
185
|
+
expect(f.codeBytes).toBeGreaterThan(0);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("flags a terse instruction", () => {
|
|
189
|
+
const terse = extractFeatures(req([SYSTEM, { role: "user", content: "bump the version" }]), 100);
|
|
190
|
+
expect(terse.isTerseInstruction).toBe(true);
|
|
191
|
+
const verbose = extractFeatures(
|
|
192
|
+
req([
|
|
193
|
+
SYSTEM,
|
|
194
|
+
{
|
|
195
|
+
role: "user",
|
|
196
|
+
content:
|
|
197
|
+
"I need you to walk through the whole retry subsystem, explain how the backoff interacts with the circuit breaker, and then propose a design that makes both testable in isolation.",
|
|
198
|
+
},
|
|
199
|
+
]),
|
|
200
|
+
100,
|
|
201
|
+
);
|
|
202
|
+
expect(verbose.isTerseInstruction).toBe(false);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("carries through image presence and requested reasoning", () => {
|
|
206
|
+
const f = extractFeatures(
|
|
207
|
+
parseChatRequest(
|
|
208
|
+
{
|
|
209
|
+
model: "auto",
|
|
210
|
+
reasoning_effort: "high",
|
|
211
|
+
messages: [
|
|
212
|
+
{
|
|
213
|
+
role: "user",
|
|
214
|
+
content: [
|
|
215
|
+
{ type: "text", text: "what is this" },
|
|
216
|
+
{ type: "image_url", image_url: { url: "data:image/png;base64,AA" } },
|
|
217
|
+
],
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
},
|
|
221
|
+
new Headers(),
|
|
222
|
+
),
|
|
223
|
+
100,
|
|
224
|
+
);
|
|
225
|
+
expect(f.hasImages).toBe(true);
|
|
226
|
+
expect(f.requestedReasoning).toBe("high");
|
|
227
|
+
});
|
|
228
|
+
});
|