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.
@@ -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", () => {
@@ -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", () => {
@@ -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
@@ -199,6 +199,7 @@ function requestOf(row: Row, f: Features): NormRequest {
199
199
  agentdoxScope: "",
200
200
  agentdoxGroup: "",
201
201
  agentdoxPersonal: "",
202
+ agentdoxOrigin: "",
202
203
  isSubagent: false,
203
204
  requestedModel: row.requested_model,
204
205
  messages,