auto-model-router 0.16.0 → 0.18.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/.claude/skills/agentdox/SKILL.md +13 -3
- package/.mcp.json +1 -1
- package/.omp-plugin/marketplace.json +2 -2
- package/CLAUDE.md +2 -2
- package/README.md +32 -1
- package/docs/AGENTDOX-BRIDGE.md +18 -1
- package/omp-extension/embed-logic.ts +91 -1
- package/omp-extension/remote-logic.ts +4 -1
- package/omp-extension/router-embed.ts +16 -4
- package/package.json +1 -1
- package/src/catalog/composite.ts +12 -1
- package/src/cli/config-cmd.ts +5 -2
- package/src/cli/connect.ts +6 -2
- package/src/context/scope.ts +63 -0
- package/src/lib.ts +1 -0
- package/src/router/candidates.ts +31 -16
- package/src/server/advise.ts +1 -0
- package/src/server/catalog-view.ts +130 -0
- package/src/server/digest.ts +1 -0
- package/src/server/http.ts +38 -0
- package/src/server/providers.ts +2 -2
- package/src/wire/openai/request.ts +7 -1
- package/src/wire/types.ts +9 -0
- package/test/anthropic-wire.test.ts +6 -0
- package/test/catalog-view.test.ts +249 -0
- package/test/embed-logic.test.ts +86 -1
- package/test/escalate.test.ts +1 -0
- package/test/failover.test.ts +1 -0
- package/test/models-yml.test.ts +10 -0
- package/test/remote.test.ts +8 -1
- package/test/scope.test.ts +54 -1
- package/test/turn.test.ts +1 -0
- package/tools/replay.ts +1 -0
package/test/models-yml.test.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
import { parse as parseYaml } from "yaml";
|
|
3
3
|
|
|
4
4
|
import { loadConfig } from "../src/config/load.ts";
|
|
5
|
+
import { ORIGIN_ENV, SCOPE_ENV } from "../src/context/scope.ts";
|
|
5
6
|
import {
|
|
6
7
|
BEGIN_GUARD,
|
|
7
8
|
END_GUARD,
|
|
@@ -68,6 +69,15 @@ describe("renderProviderBlock", () => {
|
|
|
68
69
|
expect(BLOCK).toContain("auth: none");
|
|
69
70
|
expect(BLOCK).toContain("/v1");
|
|
70
71
|
});
|
|
72
|
+
|
|
73
|
+
test("with the bridge on, the scope and origin headers name the variables the extension sets", () => {
|
|
74
|
+
// The file is machine-wide, so neither can be a literal: omp resolves a
|
|
75
|
+
// header value that names an env var per request (src/context/scope.ts).
|
|
76
|
+
const bridged = renderProviderBlock(loadConfig({ overrides: { context: { enabled: true, baseUrl: "http://agentdox:3003", token: "t" } } }), null);
|
|
77
|
+
expect(bridged).toContain(`X-Agentdox-Scope: ${SCOPE_ENV}`);
|
|
78
|
+
expect(bridged).toContain(`X-Agentdox-Origin: ${ORIGIN_ENV}`);
|
|
79
|
+
expect(BLOCK).not.toContain("X-Agentdox-");
|
|
80
|
+
});
|
|
71
81
|
});
|
|
72
82
|
|
|
73
83
|
describe("spliceProviderBlock", () => {
|
package/test/remote.test.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { join } from "node:path";
|
|
|
6
6
|
import { addExtensions, codexBlock, connectRemote, setDotenv, type ConnectOptions } from "../src/cli/connect.ts";
|
|
7
7
|
import { parseRemoteRouter, readRemoteRouter, remoteProviderRegistration } from "../omp-extension/remote-logic.ts";
|
|
8
8
|
import { existingBlockScope, hasForeignRouterProvider, mergeModelsYml, renderRemoteModelsYml } from "../src/cli/connect.ts";
|
|
9
|
-
import { SCOPE_ENV } from "../src/context/scope.ts";
|
|
9
|
+
import { ORIGIN_ENV, SCOPE_ENV } from "../src/context/scope.ts";
|
|
10
10
|
import { refreshAndRewrite, refreshCredential, RefreshError, resolveRefreshToken, shouldRefresh } from "../src/cli/refresh.ts";
|
|
11
11
|
import { loadRefreshToken, pickStore, removeRefreshToken, saveRefreshToken } from "../src/cli/credential-store.ts";
|
|
12
12
|
import { hasRefresh, refreshAccountOf } from "../omp-extension/remote-logic.ts";
|
|
@@ -31,6 +31,9 @@ describe("remote-logic", () => {
|
|
|
31
31
|
// the machine with the right context; the remote may still override it.
|
|
32
32
|
expect(reg.headers["X-Agentdox-Scope"]).toBeUndefined();
|
|
33
33
|
expect(remoteProviderRegistration(t, "", false, { inputPerMtok: 1, outputPerMtok: 4 }, "omp-router").headers).toEqual({ "X-Agentdox-Scope": "omp-router" });
|
|
34
|
+
// The repository fingerprint rides beside the scope, and only when there is one.
|
|
35
|
+
expect(remoteProviderRegistration(t, "", false, { inputPerMtok: 1, outputPerMtok: 4 }, "omp-router", "github.com/drewappling/omp-router").headers).toEqual({ "X-Agentdox-Scope": "omp-router", "X-Agentdox-Origin": "github.com/drewappling/omp-router" });
|
|
36
|
+
expect(remoteProviderRegistration(t, "", false, { inputPerMtok: 1, outputPerMtok: 4 }, "", "github.com/drewappling/omp-router").headers).toEqual({ "X-Agentdox-Origin": "github.com/drewappling/omp-router" });
|
|
34
37
|
const dir = mkdtempSync(join(tmpdir(), "amr-remote-"));
|
|
35
38
|
expect(readRemoteRouter(dir)).toBeNull();
|
|
36
39
|
writeFileSync(join(dir, "remote.json"), JSON.stringify({ url: "https://t", key: "k" }));
|
|
@@ -126,12 +129,16 @@ describe("omp models.yml for a remote router", () => {
|
|
|
126
129
|
// Machine-wide file: the header names the env var the extension sets per
|
|
127
130
|
// workspace, so the MAIN model's turns carry each repo's own scope.
|
|
128
131
|
expect(block).toContain(`X-Agentdox-Scope: ${SCOPE_ENV}`);
|
|
132
|
+
// The origin names its own variable the same way, on every block.
|
|
133
|
+
expect(block).toContain(`X-Agentdox-Origin: ${ORIGIN_ENV}`);
|
|
129
134
|
expect(existingBlockScope(mergeModelsYml("", block))).toBe("");
|
|
130
135
|
// --scope pins one slug for the whole machine.
|
|
131
136
|
const pinned = renderRemoteModelsYml("https://team.example", "k", BLEND, "omp-router");
|
|
132
137
|
expect(pinned).toContain("X-Agentdox-Scope: omp-router");
|
|
133
138
|
expect(pinned).not.toContain(SCOPE_ENV);
|
|
134
139
|
expect(existingBlockScope(mergeModelsYml("", pinned))).toBe("omp-router");
|
|
140
|
+
// A pinned scope does not pin an origin: the repository is still the workspace's.
|
|
141
|
+
expect(pinned).toContain(`X-Agentdox-Origin: ${ORIGIN_ENV}`);
|
|
135
142
|
});
|
|
136
143
|
|
|
137
144
|
test("merging keeps other providers, replaces our own block, and is idempotent", () => {
|
package/test/scope.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { SCOPE_ENV, acceptScope, isScopeSlug } from "../src/context/scope.ts";
|
|
2
|
+
import { ORIGIN_ENV, SCOPE_ENV, acceptOrigin, acceptScope, isOrigin, isScopeSlug, normalizeOrigin } from "../src/context/scope.ts";
|
|
3
3
|
import { parseChatRequest } from "../src/wire/openai/request.ts";
|
|
4
4
|
|
|
5
5
|
describe("the scope a request may name", () => {
|
|
@@ -49,3 +49,56 @@ describe("the scope a request may name", () => {
|
|
|
49
49
|
expect(parse({ "x-agentdox-group": "group.g1" }).agentdoxGroup).toBe("group.g1");
|
|
50
50
|
});
|
|
51
51
|
});
|
|
52
|
+
|
|
53
|
+
describe("the origin a request may name", () => {
|
|
54
|
+
test("normalizeOrigin reduces every remote form to host/path, and a local path to nothing", () => {
|
|
55
|
+
// The same repository over https and ssh is one fingerprint.
|
|
56
|
+
expect(normalizeOrigin("https://github.com/DrewAppling/omp-router.git")).toBe("github.com/drewappling/omp-router");
|
|
57
|
+
expect(normalizeOrigin("git@github.com:drewappling/omp-router.git")).toBe("github.com/drewappling/omp-router");
|
|
58
|
+
expect(normalizeOrigin("ssh://git@gitlab.example.com:2222/team/api/")).toBe("gitlab.example.com/team/api");
|
|
59
|
+
// Credentials and the port never reach the value.
|
|
60
|
+
expect(normalizeOrigin("https://user:p4ss@host.example:8443/a/b.git")).toBe("host.example/a/b");
|
|
61
|
+
expect(normalizeOrigin("host.example:repo")).toBe("host.example/repo");
|
|
62
|
+
expect(normalizeOrigin(" https://github.com/a/b/.git ")).toBe("github.com/a/b");
|
|
63
|
+
// No host, no fingerprint: a local clone says nothing about the project.
|
|
64
|
+
expect(normalizeOrigin("file:///e/projects/x")).toBe("");
|
|
65
|
+
expect(normalizeOrigin("/local/path")).toBe("");
|
|
66
|
+
expect(normalizeOrigin("C:\\path")).toBe("");
|
|
67
|
+
expect(normalizeOrigin("C:\\Users\\drew\\repo")).toBe("");
|
|
68
|
+
expect(normalizeOrigin("C:/path")).toBe("");
|
|
69
|
+
expect(normalizeOrigin("https://github.com/")).toBe("");
|
|
70
|
+
expect(normalizeOrigin("")).toBe("");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("isOrigin: host/path only; the env-var sentinel never passes", () => {
|
|
74
|
+
expect(isOrigin("github.com/drewappling/omp-router")).toBe(true);
|
|
75
|
+
expect(isOrigin("gitlab.example.com/team/sub/api")).toBe(true);
|
|
76
|
+
// omp sends the header's literal value when the variable it names is
|
|
77
|
+
// unset: the NAME must never become a fingerprint.
|
|
78
|
+
expect(isOrigin(ORIGIN_ENV)).toBe(false);
|
|
79
|
+
expect(isOrigin("github.com")).toBe(false);
|
|
80
|
+
expect(isOrigin("GitHub.com/a/b")).toBe(false);
|
|
81
|
+
expect(isOrigin("https://github.com/a/b")).toBe(false);
|
|
82
|
+
expect(isOrigin("github.com/a/b/")).toBe(false);
|
|
83
|
+
expect(isOrigin("")).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("acceptOrigin trims and rejects", () => {
|
|
87
|
+
expect(acceptOrigin(" github.com/a/b ")).toBe("github.com/a/b");
|
|
88
|
+
expect(acceptOrigin(ORIGIN_ENV)).toBe("");
|
|
89
|
+
expect(acceptOrigin("https://github.com/a/b")).toBe("");
|
|
90
|
+
expect(acceptOrigin(null)).toBe("");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("the wire parser reads X-Agentdox-Origin by that rule and nothing else changes", () => {
|
|
94
|
+
const body = { model: "auto", messages: [{ role: "user", content: "hi" }] };
|
|
95
|
+
const parse = (h: Record<string, string>) => parseChatRequest(body, new Headers(h));
|
|
96
|
+
expect(parse({ "x-agentdox-scope": "omp-router" }).agentdoxOrigin).toBe("");
|
|
97
|
+
const both = parse({ "x-agentdox-scope": "omp-router", "x-agentdox-origin": "github.com/drewappling/omp-router" });
|
|
98
|
+
expect(both.agentdoxScope).toBe("omp-router");
|
|
99
|
+
expect(both.agentdoxOrigin).toBe("github.com/drewappling/omp-router");
|
|
100
|
+
// The sentinel and a raw URL are dropped, never passed through.
|
|
101
|
+
expect(parse({ "x-agentdox-origin": ORIGIN_ENV }).agentdoxOrigin).toBe("");
|
|
102
|
+
expect(parse({ "x-agentdox-origin": "https://github.com/a/b" }).agentdoxOrigin).toBe("");
|
|
103
|
+
});
|
|
104
|
+
});
|
package/test/turn.test.ts
CHANGED
|
@@ -98,6 +98,7 @@ function mkReq(): NormRequest {
|
|
|
98
98
|
agentdoxScope: "",
|
|
99
99
|
agentdoxGroup: "",
|
|
100
100
|
agentdoxPersonal: "",
|
|
101
|
+
agentdoxOrigin: "",
|
|
101
102
|
isSubagent: false,
|
|
102
103
|
requestedModel: "auto",
|
|
103
104
|
messages: [{ role: "user", text: "hi", images: 0, textBytes: 2, toolCalls: [] }],
|
package/tools/replay.ts
CHANGED