@velum-labs/routekit-daemon 1.3.2 → 1.5.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/dist/account-enroll-service.d.ts +1 -3
- package/dist/account-enroll-service.js +262 -262
- package/dist/account-mutation-service.d.ts +1 -3
- package/dist/account-mutation-service.js +302 -283
- package/dist/account-query-service.d.ts +1 -3
- package/dist/account-query-service.js +88 -86
- package/dist/call-attribution-store.js +544 -1
- package/dist/control-dispatch.js +9 -15
- package/dist/daemon-bootstrap-preflight.js +1 -1
- package/dist/daemon-control-handlers.d.ts +1 -1
- package/dist/daemon-control-handlers.js +24 -27
- package/dist/daemon-generations.d.ts +14 -28
- package/dist/daemon-generations.js +114 -53
- package/dist/daemon-lifecycle-service.d.ts +1 -3
- package/dist/daemon-lifecycle-service.js +88 -94
- package/dist/daemon-maintenance.d.ts +27 -49
- package/dist/daemon-maintenance.js +5 -1
- package/dist/daemon-options.d.ts +3 -2
- package/dist/daemon-policy-context.d.ts +1 -0
- package/dist/daemon-policy-context.js +1 -0
- package/dist/daemon-runtime-state.d.ts +1 -1
- package/dist/daemon-runtime-state.js +2 -3
- package/dist/daemon-state-context.d.ts +1 -1
- package/dist/daemon-state-context.js +1 -38
- package/dist/doctor-service.d.ts +1 -3
- package/dist/doctor-service.js +87 -91
- package/dist/effect/daemon-live.d.ts +7 -6
- package/dist/effect/daemon-live.js +57 -135
- package/dist/eval-routing-service.d.ts +1 -3
- package/dist/eval-routing-service.js +45 -69
- package/dist/eval-session-execution-store.d.ts +24 -0
- package/dist/eval-session-execution-store.js +253 -0
- package/dist/host-protocol.d.ts +1 -0
- package/dist/host.js +3 -0
- package/dist/internal-providers/provider-anthropic/index.d.ts +23 -0
- package/dist/internal-providers/provider-anthropic/index.js +42 -0
- package/dist/internal-providers/provider-anthropic/test/provider.test.js +33 -0
- package/dist/internal-providers/provider-azure/index.d.ts +25 -0
- package/dist/internal-providers/provider-azure/index.js +101 -0
- package/dist/internal-providers/provider-azure/test/provider.test.js +83 -0
- package/dist/internal-providers/provider-bedrock/codec.d.ts +11 -0
- package/dist/internal-providers/provider-bedrock/codec.js +484 -0
- package/dist/internal-providers/provider-bedrock/connection-settings.d.ts +6 -0
- package/dist/internal-providers/provider-bedrock/connection-settings.js +41 -0
- package/dist/internal-providers/provider-bedrock/connection.d.ts +17 -0
- package/dist/internal-providers/provider-bedrock/connection.js +143 -0
- package/dist/internal-providers/provider-bedrock/index.d.ts +27 -0
- package/dist/internal-providers/provider-bedrock/index.js +47 -0
- package/dist/internal-providers/provider-bedrock/source.d.ts +39 -0
- package/dist/internal-providers/provider-bedrock/source.js +454 -0
- package/dist/internal-providers/provider-bedrock/test/bedrock-source.test.d.ts +1 -0
- package/dist/internal-providers/provider-bedrock/test/bedrock-source.test.js +786 -0
- package/dist/internal-providers/provider-bedrock/test/connection.test.d.ts +1 -0
- package/dist/internal-providers/provider-bedrock/test/connection.test.js +249 -0
- package/dist/internal-providers/provider-bedrock/test/workload-credentials.test.d.ts +1 -0
- package/dist/internal-providers/provider-bedrock/test/workload-credentials.test.js +29 -0
- package/dist/internal-providers/provider-bedrock/workload-credentials.d.ts +5 -0
- package/dist/internal-providers/provider-bedrock/workload-credentials.js +14 -0
- package/dist/internal-providers/provider-claude-code/index.d.ts +6 -0
- package/dist/internal-providers/provider-claude-code/index.js +4 -0
- package/dist/internal-providers/provider-claude-code/module-api.d.ts +25 -0
- package/dist/internal-providers/provider-claude-code/module-api.js +31 -0
- package/dist/internal-providers/provider-claude-code/relay.d.ts +20 -0
- package/dist/internal-providers/provider-claude-code/relay.js +115 -0
- package/dist/internal-providers/provider-claude-code/subscription.d.ts +2 -0
- package/dist/internal-providers/provider-claude-code/subscription.js +194 -0
- package/dist/internal-providers/provider-claude-code/test/http-submission.test.d.ts +1 -0
- package/dist/internal-providers/provider-claude-code/test/http-submission.test.js +140 -0
- package/dist/internal-providers/provider-claude-code/test/relay.test.d.ts +1 -0
- package/dist/internal-providers/provider-claude-code/test/relay.test.js +379 -0
- package/dist/internal-providers/provider-claude-code/test/subscription.test.d.ts +1 -0
- package/dist/internal-providers/provider-claude-code/test/subscription.test.js +256 -0
- package/dist/internal-providers/provider-cliproxy/index.d.ts +9 -0
- package/dist/internal-providers/provider-cliproxy/index.js +15 -0
- package/dist/internal-providers/provider-codex/index.d.ts +6 -0
- package/dist/internal-providers/provider-codex/index.js +7 -0
- package/dist/internal-providers/provider-codex/module-api.d.ts +25 -0
- package/dist/internal-providers/provider-codex/module-api.js +31 -0
- package/dist/internal-providers/provider-codex/relay.d.ts +123 -0
- package/dist/internal-providers/provider-codex/relay.js +261 -0
- package/dist/internal-providers/provider-codex/subscription.d.ts +2 -0
- package/dist/internal-providers/provider-codex/subscription.js +555 -0
- package/dist/internal-providers/provider-codex/test/http-submission.test.d.ts +1 -0
- package/dist/internal-providers/provider-codex/test/http-submission.test.js +142 -0
- package/dist/internal-providers/provider-codex/test/relay.test.d.ts +1 -0
- package/dist/internal-providers/provider-codex/test/relay.test.js +600 -0
- package/dist/internal-providers/provider-codex/test/subscription.test.d.ts +1 -0
- package/dist/internal-providers/provider-codex/test/subscription.test.js +70 -0
- package/dist/internal-providers/provider-codex/test/translated-content.test.d.ts +1 -0
- package/dist/internal-providers/provider-codex/test/translated-content.test.js +91 -0
- package/dist/internal-providers/provider-custom/index.d.ts +9 -0
- package/dist/internal-providers/provider-custom/index.js +15 -0
- package/dist/internal-providers/provider-google/index.d.ts +23 -0
- package/dist/internal-providers/provider-google/index.js +43 -0
- package/dist/internal-providers/provider-google/test/provider.test.d.ts +1 -0
- package/dist/internal-providers/provider-google/test/provider.test.js +34 -0
- package/dist/internal-providers/provider-google-vertex/index.d.ts +28 -0
- package/dist/internal-providers/provider-google-vertex/index.js +137 -0
- package/dist/internal-providers/provider-google-vertex/test/provider.test.d.ts +1 -0
- package/dist/internal-providers/provider-google-vertex/test/provider.test.js +85 -0
- package/dist/internal-providers/provider-openai/documented-reasoning.d.ts +9 -0
- package/dist/internal-providers/provider-openai/documented-reasoning.js +26 -0
- package/dist/internal-providers/provider-openai/index.d.ts +23 -0
- package/dist/internal-providers/provider-openai/index.js +74 -0
- package/dist/internal-providers/provider-openai/test/provider.test.d.ts +1 -0
- package/dist/internal-providers/provider-openai/test/provider.test.js +268 -0
- package/dist/internal-providers/provider-openai-compatible/index.d.ts +9 -0
- package/dist/internal-providers/provider-openai-compatible/index.js +15 -0
- package/dist/internal-providers/provider-openai-compatible/test/provider.test.d.ts +1 -0
- package/dist/internal-providers/provider-openai-compatible/test/provider.test.js +40 -0
- package/dist/internal-providers/provider-openrouter/connection.d.ts +5 -0
- package/dist/internal-providers/provider-openrouter/connection.js +143 -0
- package/dist/internal-providers/provider-openrouter/index.d.ts +28 -0
- package/dist/internal-providers/provider-openrouter/index.js +42 -0
- package/dist/internal-providers/provider-openrouter/pricing.d.ts +4 -0
- package/dist/internal-providers/provider-openrouter/pricing.js +108 -0
- package/dist/internal-providers/provider-openrouter/test/connection.test.d.ts +1 -0
- package/dist/internal-providers/provider-openrouter/test/connection.test.js +71 -0
- package/dist/internal-providers/provider-openrouter/test/pricing.test.d.ts +1 -0
- package/dist/internal-providers/provider-openrouter/test/pricing.test.js +45 -0
- package/dist/internal-providers/provider-openrouter/test/provider.test.d.ts +1 -0
- package/dist/internal-providers/provider-openrouter/test/provider.test.js +141 -0
- package/dist/internal-providers/provider-openrouter/test/validation.test.d.ts +1 -0
- package/dist/internal-providers/provider-openrouter/test/validation.test.js +225 -0
- package/dist/launcher-service.d.ts +3 -7
- package/dist/launcher-service.js +144 -133
- package/dist/provider-modules.d.ts +6 -0
- package/dist/provider-modules.js +55 -0
- package/dist/provider-query-service.d.ts +1 -3
- package/dist/provider-query-service.js +140 -143
- package/dist/router-generation-service.d.ts +1 -3
- package/dist/router-generation-service.js +83 -78
- package/dist/services/eval-session/service.d.ts +20 -8
- package/dist/services/eval-session/service.js +410 -40
- package/dist/services/gateway-generation/service.d.ts +7 -3
- package/dist/services/gateway-generation/service.js +126 -63
- package/dist/services/generations/service.d.ts +3 -3
- package/dist/services/generations/service.js +5 -1
- package/dist/telemetry-application-service.d.ts +1 -3
- package/dist/telemetry-application-service.js +41 -45
- package/dist/test/call-attribution-store.test.js +186 -0
- package/dist/test/daemon-accounts.test.js +2 -2
- package/dist/test/daemon-data-plane.test.js +24 -45
- package/dist/test/daemon-fixtures.js +1 -1
- package/dist/test/daemon-generation-transaction.test.js +66 -2
- package/dist/test/daemon-runtime-state.test.js +28 -1
- package/dist/test/effect-daemon.test.js +38 -0
- package/dist/test/eval-routing-application-service.test.js +6 -6
- package/dist/test/eval-session-execution.test.d.ts +1 -0
- package/dist/test/eval-session-execution.test.js +356 -0
- package/dist/test/eval-session-service.test.js +70 -1
- package/dist/test/host-worker-session.test.js +1 -0
- package/dist/test/launcher-session.test.d.ts +1 -0
- package/dist/test/launcher-session.test.js +87 -0
- package/dist/test/leaderboard.test.js +25 -1
- package/dist/test/provider-modules.test.d.ts +1 -0
- package/dist/test/provider-modules.test.js +65 -0
- package/dist/test/routekit-upstream-sources.test.d.ts +1 -0
- package/dist/test/routekit-upstream-sources.test.js +40 -0
- package/dist/test/runtime-composition-types.test.js +11 -0
- package/dist/token-application-service.d.ts +1 -3
- package/dist/token-application-service.js +38 -44
- package/dist/worker.js +5 -3
- package/package.json +35 -11
- package/dist/application-services.d.ts +0 -10
- package/dist/application-services.js +0 -22
- package/dist/daemon-lifecycle.d.ts +0 -54
- package/dist/daemon-lifecycle.js +0 -177
- package/dist/test/application-services.test.js +0 -135
- package/dist/test/daemon-lifecycle.test.js +0 -182
- /package/dist/{test/application-services.test.d.ts → internal-providers/provider-anthropic/test/provider.test.d.ts} +0 -0
- /package/dist/{test/daemon-lifecycle.test.d.ts → internal-providers/provider-azure/test/provider.test.d.ts} +0 -0
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
import { codexProvider } from "../subscription.js";
|
|
2
|
+
import { CodexBackendRelay, codexRelayAuth } from "../relay.js";
|
|
3
|
+
import assert from "node:assert/strict";
|
|
4
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { createServer } from "node:http";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { test } from "node:test";
|
|
9
|
+
import { startGateway } from "@velum-labs/routekit-gateway";
|
|
10
|
+
import { borrowedBackendPorts, staticBackendModelPort } from "@velum-labs/routekit-provider-core";
|
|
11
|
+
import { runRouteKitEffect } from "@velum-labs/routekit-runtime/effect";
|
|
12
|
+
import { Effect } from "effect";
|
|
13
|
+
import { RelayOnlyBackend, relayPorts, SubscriptionAccountSet } from "@velum-labs/routekit-accounts";
|
|
14
|
+
/**
|
|
15
|
+
* Regression (ENG-620): a Codex client pointed at the gateway keeps its own
|
|
16
|
+
* stock models. The relay merges the client's live stock catalog into
|
|
17
|
+
* `GET /v1/models` and adapts stock-model Responses requests to the Codex
|
|
18
|
+
* backend using the auth material the client itself attached.
|
|
19
|
+
*/
|
|
20
|
+
const CHATGPT_HEADERS = {
|
|
21
|
+
authorization: "Bearer chatgpt-token-redacted",
|
|
22
|
+
"chatgpt-account-id": "acct_123",
|
|
23
|
+
originator: "codex_cli_rs",
|
|
24
|
+
"openai-beta": "responses=v1"
|
|
25
|
+
};
|
|
26
|
+
const STOCK = [
|
|
27
|
+
{
|
|
28
|
+
slug: "gpt-5.5",
|
|
29
|
+
display_name: "GPT-5.5",
|
|
30
|
+
description: "Flagship",
|
|
31
|
+
visibility: "list",
|
|
32
|
+
priority: 0
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
slug: "gpt-5.3-codex",
|
|
36
|
+
display_name: "gpt-5.3-codex",
|
|
37
|
+
description: "Coding",
|
|
38
|
+
visibility: "list",
|
|
39
|
+
priority: 1
|
|
40
|
+
}
|
|
41
|
+
];
|
|
42
|
+
const FALLBACK_STOCK = [
|
|
43
|
+
{
|
|
44
|
+
slug: "gpt-5.5-cached",
|
|
45
|
+
display_name: "GPT-5.5 (cached)",
|
|
46
|
+
description: "Snapshot",
|
|
47
|
+
visibility: "list",
|
|
48
|
+
priority: 0
|
|
49
|
+
}
|
|
50
|
+
];
|
|
51
|
+
async function readBody(req) {
|
|
52
|
+
const chunks = [];
|
|
53
|
+
for await (const chunk of req)
|
|
54
|
+
chunks.push(chunk);
|
|
55
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
56
|
+
}
|
|
57
|
+
async function closeServer(server) {
|
|
58
|
+
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
|
|
59
|
+
}
|
|
60
|
+
/** A stand-in for the ChatGPT Codex backend (`/models` + streaming `/responses`). */
|
|
61
|
+
async function startMockCodexBackend() {
|
|
62
|
+
const state = {
|
|
63
|
+
url: "",
|
|
64
|
+
modelsRequests: [],
|
|
65
|
+
responsesRequests: [],
|
|
66
|
+
failModels: false,
|
|
67
|
+
close: async () => { }
|
|
68
|
+
};
|
|
69
|
+
const server = createServer((req, res) => {
|
|
70
|
+
void (async () => {
|
|
71
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
72
|
+
if (req.method === "GET" && url.pathname === "/models") {
|
|
73
|
+
state.modelsRequests.push({ headers: req.headers, search: url.search });
|
|
74
|
+
if (state.failModels) {
|
|
75
|
+
res.writeHead(503, { "content-type": "application/json" });
|
|
76
|
+
res.end(JSON.stringify({ error: "unavailable" }));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
res.writeHead(200, { "content-type": "application/json", etag: 'W/"models-v1"' });
|
|
80
|
+
res.end(JSON.stringify(state.modelsPayload ?? { models: STOCK }));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (req.method === "POST" && url.pathname === "/responses") {
|
|
84
|
+
state.responsesRequests.push({
|
|
85
|
+
headers: req.headers,
|
|
86
|
+
body: JSON.parse(await readBody(req))
|
|
87
|
+
});
|
|
88
|
+
res.writeHead(200, { "content-type": "text/event-stream" });
|
|
89
|
+
res.write('event: response.output_text.delta\ndata: {"delta":"STOCK_OK"}\n\n');
|
|
90
|
+
res.write('event: response.completed\ndata: {"response":{"id":"resp_relay"}}\n\n');
|
|
91
|
+
res.end();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
res.writeHead(404).end();
|
|
95
|
+
})().catch((error) => res.writeHead(500).end(String(error)));
|
|
96
|
+
});
|
|
97
|
+
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
98
|
+
const address = server.address();
|
|
99
|
+
assert.ok(typeof address === "object" && address !== null);
|
|
100
|
+
state.url = `http://127.0.0.1:${address.port}`;
|
|
101
|
+
state.close = () => closeServer(server);
|
|
102
|
+
return state;
|
|
103
|
+
}
|
|
104
|
+
/** The merged-catalog builder a host would wire in (local entry first). */
|
|
105
|
+
function catalog(template, stock) {
|
|
106
|
+
const local = { ...template, slug: "local-primary", display_name: "local-primary", priority: 0 };
|
|
107
|
+
const rest = stock.filter((entry) => entry.slug !== "local-primary");
|
|
108
|
+
return [local, ...rest];
|
|
109
|
+
}
|
|
110
|
+
/** A minimal backend that serves exactly one local model plus one native id. */
|
|
111
|
+
function fakeBackend() {
|
|
112
|
+
const chatModels = [];
|
|
113
|
+
const backend = {
|
|
114
|
+
chatModels,
|
|
115
|
+
defaultModel: "local-primary",
|
|
116
|
+
ports: borrowedBackendPorts("local-primary"),
|
|
117
|
+
chat(body) {
|
|
118
|
+
const model = body.model ?? "local-primary";
|
|
119
|
+
chatModels.push(model);
|
|
120
|
+
return Effect.succeed(new Response(JSON.stringify({
|
|
121
|
+
id: "chatcmpl_local",
|
|
122
|
+
object: "chat.completion",
|
|
123
|
+
created: 0,
|
|
124
|
+
model,
|
|
125
|
+
choices: [
|
|
126
|
+
{
|
|
127
|
+
index: 0,
|
|
128
|
+
message: { role: "assistant", content: "LOCAL_OK" },
|
|
129
|
+
finish_reason: "stop"
|
|
130
|
+
}
|
|
131
|
+
],
|
|
132
|
+
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }
|
|
133
|
+
}), { status: 200, headers: { "content-type": "application/json" } }));
|
|
134
|
+
},
|
|
135
|
+
models: () => Effect.succeed(new Response(JSON.stringify({
|
|
136
|
+
object: "list",
|
|
137
|
+
data: [
|
|
138
|
+
{ id: "local-primary", object: "model" },
|
|
139
|
+
{ id: "gpt-native", object: "model" }
|
|
140
|
+
]
|
|
141
|
+
}), { status: 200, headers: { "content-type": "application/json" } })),
|
|
142
|
+
embeddings: () => Effect.succeed(new Response("{}", { status: 501 }))
|
|
143
|
+
};
|
|
144
|
+
backend.ports = {
|
|
145
|
+
models: {
|
|
146
|
+
...staticBackendModelPort(backend.defaultModel),
|
|
147
|
+
kind: "model-catalog",
|
|
148
|
+
list: () => ["local-primary", "gpt-native"],
|
|
149
|
+
resolve: (requested) => (requested === "gpt-native" ? "gpt-native" : "local-primary"),
|
|
150
|
+
serves: (model) => model === "local-primary" || model === "gpt-native"
|
|
151
|
+
},
|
|
152
|
+
responses: { kind: "unsupported" },
|
|
153
|
+
lifecycle: { kind: "borrowed" }
|
|
154
|
+
};
|
|
155
|
+
return backend;
|
|
156
|
+
}
|
|
157
|
+
async function startRelayGateway(backendUrl, authToken) {
|
|
158
|
+
const backend = fakeBackend();
|
|
159
|
+
const gateway = await startGateway({
|
|
160
|
+
backend,
|
|
161
|
+
...(authToken !== undefined ? { authToken } : {}),
|
|
162
|
+
codexRelay: relayPorts(new CodexBackendRelay({
|
|
163
|
+
backendUrl,
|
|
164
|
+
catalog,
|
|
165
|
+
fallbackStock: () => FALLBACK_STOCK,
|
|
166
|
+
logger: { warn: () => { }, error: () => { } }
|
|
167
|
+
}))
|
|
168
|
+
});
|
|
169
|
+
return { gateway, backend };
|
|
170
|
+
}
|
|
171
|
+
test("codexRelayAuth only accepts the ChatGPT bearer + account-id pair", () => {
|
|
172
|
+
assert.ok(codexRelayAuth(CHATGPT_HEADERS));
|
|
173
|
+
assert.equal(codexRelayAuth({ authorization: "Bearer sk-api-key" }), undefined);
|
|
174
|
+
assert.equal(codexRelayAuth({ "chatgpt-account-id": "acct_123" }), undefined);
|
|
175
|
+
assert.equal(codexRelayAuth({ authorization: "Basic abc", "chatgpt-account-id": "acct_123" }), undefined);
|
|
176
|
+
assert.equal(codexRelayAuth({}), undefined);
|
|
177
|
+
});
|
|
178
|
+
test("GET /v1/models with ChatGPT auth merges the live stock catalog behind local entries", async () => {
|
|
179
|
+
const upstream = await startMockCodexBackend();
|
|
180
|
+
const { gateway } = await startRelayGateway(upstream.url);
|
|
181
|
+
try {
|
|
182
|
+
const response = await fetch(`${gateway.url()}/v1/models?client_version=0.142.5`, {
|
|
183
|
+
headers: CHATGPT_HEADERS
|
|
184
|
+
});
|
|
185
|
+
assert.equal(response.status, 200);
|
|
186
|
+
assert.equal(response.headers.get("etag"), 'W/"models-v1"', "upstream ETag forwarded");
|
|
187
|
+
const body = (await response.json());
|
|
188
|
+
// Codex's ModelInfo catalog: local entries first, then the live stock list.
|
|
189
|
+
assert.deepEqual(body.models.map((entry) => entry.slug), ["local-primary", "gpt-native", "gpt-5.5", "gpt-5.3-codex"]);
|
|
190
|
+
// OpenAI-shape clients still see `data`, extended with the stock slugs.
|
|
191
|
+
assert.deepEqual(body.data.map((entry) => entry.id), ["local-primary", "gpt-native", "gpt-5.5", "gpt-5.3-codex"]);
|
|
192
|
+
// The upstream fetch carried the client's own auth + originator headers.
|
|
193
|
+
const seen = upstream.modelsRequests[0];
|
|
194
|
+
assert.ok(seen);
|
|
195
|
+
assert.equal(seen.headers.authorization, CHATGPT_HEADERS.authorization);
|
|
196
|
+
assert.equal(seen.headers["chatgpt-account-id"], CHATGPT_HEADERS["chatgpt-account-id"]);
|
|
197
|
+
assert.equal(seen.headers.originator, "codex_cli_rs");
|
|
198
|
+
assert.equal(seen.search, "?client_version=0.142.5", "query forwarded verbatim");
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
await Effect.runPromise(gateway.close);
|
|
202
|
+
await upstream.close();
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
test("GET /v1/models falls back to the stock snapshot without auth or when upstream fails", async () => {
|
|
206
|
+
const upstream = await startMockCodexBackend();
|
|
207
|
+
const { gateway } = await startRelayGateway(upstream.url);
|
|
208
|
+
try {
|
|
209
|
+
// No relayable auth: no upstream call, snapshot merge instead.
|
|
210
|
+
const anonymous = (await (await fetch(`${gateway.url()}/v1/models`)).json());
|
|
211
|
+
assert.deepEqual(anonymous.models.map((entry) => entry.slug), ["local-primary", "gpt-native", "gpt-5.5-cached"]);
|
|
212
|
+
assert.equal(upstream.modelsRequests.length, 0);
|
|
213
|
+
// Upstream down: authenticated fetch degrades to the same snapshot merge.
|
|
214
|
+
upstream.failModels = true;
|
|
215
|
+
const degraded = (await (await fetch(`${gateway.url()}/v1/models`, { headers: CHATGPT_HEADERS })).json());
|
|
216
|
+
assert.deepEqual(degraded.models.map((entry) => entry.slug), ["local-primary", "gpt-native", "gpt-5.5-cached"]);
|
|
217
|
+
}
|
|
218
|
+
finally {
|
|
219
|
+
await Effect.runPromise(gateway.close);
|
|
220
|
+
await upstream.close();
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
test("GET /v1/models validates the upstream shape: bad envelopes fall back, bad entries are dropped", async () => {
|
|
224
|
+
const upstream = await startMockCodexBackend();
|
|
225
|
+
const { gateway } = await startRelayGateway(upstream.url);
|
|
226
|
+
try {
|
|
227
|
+
// An unexpected envelope (no `models` array) degrades to the snapshot
|
|
228
|
+
// merge instead of trusting an arbitrary shape.
|
|
229
|
+
upstream.modelsPayload = { data: [{ id: "not-the-codex-shape" }] };
|
|
230
|
+
const degraded = (await (await fetch(`${gateway.url()}/v1/models`, { headers: CHATGPT_HEADERS })).json());
|
|
231
|
+
assert.deepEqual(degraded.models.map((entry) => entry.slug), ["local-primary", "gpt-native", "gpt-5.5-cached"]);
|
|
232
|
+
// Per-entry salvage: malformed entries (no string slug, non-objects) are
|
|
233
|
+
// dropped without costing the rest of the live catalog.
|
|
234
|
+
upstream.modelsPayload = {
|
|
235
|
+
models: [STOCK[0], { display_name: "no slug" }, null, "nope", { slug: "" }, STOCK[1]]
|
|
236
|
+
};
|
|
237
|
+
const salvaged = (await (await fetch(`${gateway.url()}/v1/models`, { headers: CHATGPT_HEADERS })).json());
|
|
238
|
+
assert.deepEqual(salvaged.models.map((entry) => entry.slug), ["local-primary", "gpt-native", "gpt-5.5", "gpt-5.3-codex"]);
|
|
239
|
+
}
|
|
240
|
+
finally {
|
|
241
|
+
await Effect.runPromise(gateway.close);
|
|
242
|
+
await upstream.close();
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
test("POST /v1/responses normalizes public string input under the client's own auth", async () => {
|
|
246
|
+
const upstream = await startMockCodexBackend();
|
|
247
|
+
const { gateway, backend } = await startRelayGateway(upstream.url);
|
|
248
|
+
try {
|
|
249
|
+
const response = await fetch(`${gateway.url()}/v1/responses`, {
|
|
250
|
+
method: "POST",
|
|
251
|
+
headers: { ...CHATGPT_HEADERS, "content-type": "application/json" },
|
|
252
|
+
body: JSON.stringify({
|
|
253
|
+
model: "gpt-5.3-codex",
|
|
254
|
+
input: "hello",
|
|
255
|
+
stream: true,
|
|
256
|
+
max_output_tokens: 64
|
|
257
|
+
})
|
|
258
|
+
});
|
|
259
|
+
assert.equal(response.status, 200);
|
|
260
|
+
assert.match(response.headers.get("content-type") ?? "", /text\/event-stream/);
|
|
261
|
+
const text = await response.text();
|
|
262
|
+
assert.match(text, /STOCK_OK/);
|
|
263
|
+
assert.match(text, /response\.completed/);
|
|
264
|
+
// The upstream saw canonical item-list input and the client's own auth headers.
|
|
265
|
+
const seen = upstream.responsesRequests[0];
|
|
266
|
+
assert.ok(seen);
|
|
267
|
+
assert.equal(seen.headers.authorization, CHATGPT_HEADERS.authorization);
|
|
268
|
+
assert.equal(seen.headers["chatgpt-account-id"], CHATGPT_HEADERS["chatgpt-account-id"]);
|
|
269
|
+
assert.deepEqual(seen.body, {
|
|
270
|
+
model: "gpt-5.3-codex",
|
|
271
|
+
input: [
|
|
272
|
+
{
|
|
273
|
+
type: "message",
|
|
274
|
+
role: "user",
|
|
275
|
+
content: [{ type: "input_text", text: "hello" }]
|
|
276
|
+
}
|
|
277
|
+
],
|
|
278
|
+
stream: true,
|
|
279
|
+
max_output_tokens: 64
|
|
280
|
+
});
|
|
281
|
+
// Nothing leaked into the local backend for a stock pick.
|
|
282
|
+
assert.deepEqual(backend.chatModels, []);
|
|
283
|
+
}
|
|
284
|
+
finally {
|
|
285
|
+
await Effect.runPromise(gateway.close);
|
|
286
|
+
await upstream.close();
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
test("POST /v1/responses preserves native item lists and continuation fields", async () => {
|
|
290
|
+
const upstream = await startMockCodexBackend();
|
|
291
|
+
const relay = new CodexBackendRelay({
|
|
292
|
+
backendUrl: upstream.url,
|
|
293
|
+
catalog,
|
|
294
|
+
fallbackStock: () => FALLBACK_STOCK,
|
|
295
|
+
logger: { warn: () => { }, error: () => { } }
|
|
296
|
+
});
|
|
297
|
+
const input = [
|
|
298
|
+
{
|
|
299
|
+
type: "message",
|
|
300
|
+
role: "user",
|
|
301
|
+
content: [{ type: "input_text", text: "Use the tool." }]
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
type: "reasoning",
|
|
305
|
+
id: "rs_1",
|
|
306
|
+
encrypted_content: "provider-owned-ciphertext",
|
|
307
|
+
summary: []
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
type: "function_call",
|
|
311
|
+
id: "fc_1",
|
|
312
|
+
call_id: "call_1",
|
|
313
|
+
name: "read_file",
|
|
314
|
+
arguments: '{"path":"README.md"}'
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
type: "function_call_output",
|
|
318
|
+
call_id: "call_1",
|
|
319
|
+
output: "# RouteKit"
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
type: "item_reference",
|
|
323
|
+
id: "msg_prior"
|
|
324
|
+
}
|
|
325
|
+
];
|
|
326
|
+
try {
|
|
327
|
+
const body = {
|
|
328
|
+
model: "gpt-5.3-codex",
|
|
329
|
+
input,
|
|
330
|
+
previous_response_id: "resp_previous",
|
|
331
|
+
include: ["reasoning.encrypted_content"],
|
|
332
|
+
stream: true
|
|
333
|
+
};
|
|
334
|
+
const response = await runRouteKitEffect(relay.relayResponses({ ...CHATGPT_HEADERS, "content-type": "application/json" }, body));
|
|
335
|
+
assert.equal(response.status, 200);
|
|
336
|
+
await response.text();
|
|
337
|
+
assert.deepEqual(upstream.responsesRequests[0]?.body, body);
|
|
338
|
+
}
|
|
339
|
+
finally {
|
|
340
|
+
await upstream.close();
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
test("POST /v1/responses keeps locally served models local and unknown models local without auth", async () => {
|
|
344
|
+
const upstream = await startMockCodexBackend();
|
|
345
|
+
const { gateway, backend } = await startRelayGateway(upstream.url);
|
|
346
|
+
try {
|
|
347
|
+
// A gateway-served model with ChatGPT auth attached: handled locally.
|
|
348
|
+
const local = await fetch(`${gateway.url()}/v1/responses`, {
|
|
349
|
+
method: "POST",
|
|
350
|
+
headers: { ...CHATGPT_HEADERS, "content-type": "application/json" },
|
|
351
|
+
body: JSON.stringify({ model: "gpt-native", input: "hello", stream: false })
|
|
352
|
+
});
|
|
353
|
+
assert.equal(local.status, 200);
|
|
354
|
+
assert.match(await local.text(), /LOCAL_OK/);
|
|
355
|
+
// An unknown model WITHOUT relayable auth: historical fold-to-default.
|
|
356
|
+
const anonymous = await fetch(`${gateway.url()}/v1/responses`, {
|
|
357
|
+
method: "POST",
|
|
358
|
+
headers: { "content-type": "application/json" },
|
|
359
|
+
body: JSON.stringify({ model: "gpt-5.3-codex", input: "hello", stream: false })
|
|
360
|
+
});
|
|
361
|
+
assert.equal(anonymous.status, 200);
|
|
362
|
+
assert.match(await anonymous.text(), /LOCAL_OK/);
|
|
363
|
+
assert.equal(upstream.responsesRequests.length, 0, "nothing was relayed");
|
|
364
|
+
assert.deepEqual(backend.chatModels, ["gpt-native", "local-primary"]);
|
|
365
|
+
}
|
|
366
|
+
finally {
|
|
367
|
+
await Effect.runPromise(gateway.close);
|
|
368
|
+
await upstream.close();
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
test("a gateway auth token disables the relay entirely", async () => {
|
|
372
|
+
const upstream = await startMockCodexBackend();
|
|
373
|
+
const { gateway } = await startRelayGateway(upstream.url, "gateway-token");
|
|
374
|
+
try {
|
|
375
|
+
const models = (await (await fetch(`${gateway.url()}/v1/models`, {
|
|
376
|
+
headers: { authorization: "Bearer gateway-token" }
|
|
377
|
+
})).json());
|
|
378
|
+
assert.deepEqual(models.models.map((entry) => entry.slug), ["local-primary", "gpt-native"], "gateway auth exposes only the local dual-shape catalog");
|
|
379
|
+
assert.deepEqual(models.data.map((entry) => entry.id), ["local-primary", "gpt-native"]);
|
|
380
|
+
assert.equal(upstream.modelsRequests.length, 0);
|
|
381
|
+
}
|
|
382
|
+
finally {
|
|
383
|
+
await Effect.runPromise(gateway.close);
|
|
384
|
+
await upstream.close();
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
test("server-owned Codex relay reroutes HTTP 200 terminal usage failure without leaking bytes", async () => {
|
|
388
|
+
const directory = mkdtempSync(join(tmpdir(), "routekit-codex-relay-pool-"));
|
|
389
|
+
const token = "eyJhbGciOiJub25lIn0.eyJleHAiOjk5OTk5OTk5OTl9.";
|
|
390
|
+
const outputText = "x".repeat(1_100_000);
|
|
391
|
+
for (const [name, accountId] of [
|
|
392
|
+
["a", "acct-a"],
|
|
393
|
+
["b", "acct-b"]
|
|
394
|
+
]) {
|
|
395
|
+
writeFileSync(join(directory, `${name}.json`), JSON.stringify({
|
|
396
|
+
tokens: {
|
|
397
|
+
access_token: token,
|
|
398
|
+
refresh_token: `refresh-${name}`,
|
|
399
|
+
account_id: accountId
|
|
400
|
+
}
|
|
401
|
+
}));
|
|
402
|
+
}
|
|
403
|
+
const seenAccounts = [];
|
|
404
|
+
const server = createServer((req, res) => {
|
|
405
|
+
void (async () => {
|
|
406
|
+
const accountId = String(req.headers["chatgpt-account-id"]);
|
|
407
|
+
seenAccounts.push(accountId);
|
|
408
|
+
const body = JSON.parse(await readBody(req));
|
|
409
|
+
assert.equal(body.model, "gpt-5.5");
|
|
410
|
+
assert.deepEqual(body.input, [
|
|
411
|
+
{
|
|
412
|
+
type: "message",
|
|
413
|
+
role: "user",
|
|
414
|
+
content: [{ type: "input_text", text: "hello" }]
|
|
415
|
+
}
|
|
416
|
+
]);
|
|
417
|
+
assert.equal(body.store, false);
|
|
418
|
+
assert.equal(body.stream, true);
|
|
419
|
+
assert.equal(body.max_output_tokens, undefined);
|
|
420
|
+
assert.equal(body.temperature, undefined);
|
|
421
|
+
assert.equal(body.top_p, undefined);
|
|
422
|
+
res.writeHead(200, { "content-type": "application/octet-stream" });
|
|
423
|
+
if (accountId === "acct-a") {
|
|
424
|
+
res.end(`event: response.failed\ndata: ${JSON.stringify({
|
|
425
|
+
response: {
|
|
426
|
+
error: {
|
|
427
|
+
type: "usage_limit_reached",
|
|
428
|
+
code: "weekly_limit",
|
|
429
|
+
message: "weekly usage limit reached",
|
|
430
|
+
resets_at: Math.floor(Date.now() / 1000) + 300
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
})}\n\n`);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
res.end([
|
|
437
|
+
`event: response.output_item.done\ndata: ${JSON.stringify({
|
|
438
|
+
type: "response.output_item.done",
|
|
439
|
+
output_index: 0,
|
|
440
|
+
item: {
|
|
441
|
+
type: "message",
|
|
442
|
+
id: "msg_pooled",
|
|
443
|
+
status: "completed",
|
|
444
|
+
role: "assistant",
|
|
445
|
+
content: [{ type: "output_text", text: outputText, annotations: [] }]
|
|
446
|
+
}
|
|
447
|
+
})}\n\n`,
|
|
448
|
+
`event: response.completed\ndata: ${JSON.stringify({
|
|
449
|
+
response: {
|
|
450
|
+
id: "resp_pooled",
|
|
451
|
+
model: body.model,
|
|
452
|
+
output: []
|
|
453
|
+
}
|
|
454
|
+
})}\n\n`
|
|
455
|
+
].join(""));
|
|
456
|
+
})().catch((error) => res.writeHead(500).end(String(error)));
|
|
457
|
+
});
|
|
458
|
+
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
459
|
+
const address = server.address();
|
|
460
|
+
assert.ok(typeof address === "object" && address !== null);
|
|
461
|
+
const accounts = await runRouteKitEffect(SubscriptionAccountSet.open(codexProvider(), {
|
|
462
|
+
source: { kind: "directory", path: directory },
|
|
463
|
+
strategy: "sticky"
|
|
464
|
+
}));
|
|
465
|
+
const relay = new CodexBackendRelay({
|
|
466
|
+
backendUrl: `http://127.0.0.1:${address.port}`,
|
|
467
|
+
catalog,
|
|
468
|
+
auth: { kind: "accounts", accounts }
|
|
469
|
+
});
|
|
470
|
+
const records = [];
|
|
471
|
+
const gateway = await startGateway({
|
|
472
|
+
backend: new RelayOnlyBackend(),
|
|
473
|
+
providerRelays: { codex: relayPorts(relay) },
|
|
474
|
+
provenance: { onModelCall: (record) => records.push(record) }
|
|
475
|
+
});
|
|
476
|
+
try {
|
|
477
|
+
const response = await fetch(`${gateway.url()}/v1/responses`, {
|
|
478
|
+
method: "POST",
|
|
479
|
+
headers: { "content-type": "application/json" },
|
|
480
|
+
body: JSON.stringify({
|
|
481
|
+
model: "gpt-5.5",
|
|
482
|
+
input: "hello",
|
|
483
|
+
max_output_tokens: 128,
|
|
484
|
+
temperature: 0.2,
|
|
485
|
+
top_p: 0.9
|
|
486
|
+
})
|
|
487
|
+
});
|
|
488
|
+
assert.equal(response.status, 200);
|
|
489
|
+
assert.match(response.headers.get("content-type") ?? "", /application\/json/u);
|
|
490
|
+
const responseBody = (await response.json());
|
|
491
|
+
assert.equal(responseBody.id, "resp_pooled");
|
|
492
|
+
assert.equal(responseBody.status, "completed");
|
|
493
|
+
assert.equal(responseBody.output?.[0]?.content?.[0]?.text, outputText);
|
|
494
|
+
assert.deepEqual(seenAccounts, ["acct-a", "acct-b"]);
|
|
495
|
+
const snapshot = accounts.snapshot();
|
|
496
|
+
assert.ok(snapshot.members.find((member) => member.id === "a")?.coolingUntil);
|
|
497
|
+
assert.equal(snapshot.members.find((member) => member.lastSelected)?.id, "b");
|
|
498
|
+
assert.equal(records.length, 2);
|
|
499
|
+
assert.equal(records[0]?.status, "failed");
|
|
500
|
+
assert.equal(records[1]?.status, "succeeded");
|
|
501
|
+
assert.deepEqual({
|
|
502
|
+
attempts: records[1]?.metadata?.attribution?.attempts,
|
|
503
|
+
retries: records[1]?.metadata?.attribution?.retries,
|
|
504
|
+
account_failovers: records[1]?.metadata?.attribution?.account_failovers
|
|
505
|
+
}, { attempts: 2, retries: 1, account_failovers: 1 });
|
|
506
|
+
assert.match(records[1]?.metadata?.attribution?.account?.seat ?? "", /^seat_[0-9a-f]{16}$/);
|
|
507
|
+
}
|
|
508
|
+
finally {
|
|
509
|
+
await Effect.runPromise(gateway.close);
|
|
510
|
+
await closeServer(server);
|
|
511
|
+
rmSync(directory, { recursive: true, force: true });
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
test("server-owned Codex relay retries a pre-response transport disconnect once", async () => {
|
|
515
|
+
const directory = mkdtempSync(join(tmpdir(), "routekit-codex-relay-transport-"));
|
|
516
|
+
const token = "eyJhbGciOiJub25lIn0.eyJleHAiOjk5OTk5OTk5OTl9.";
|
|
517
|
+
writeFileSync(join(directory, "a.json"), JSON.stringify({
|
|
518
|
+
tokens: {
|
|
519
|
+
access_token: token,
|
|
520
|
+
refresh_token: "refresh-a",
|
|
521
|
+
account_id: "acct-a"
|
|
522
|
+
}
|
|
523
|
+
}));
|
|
524
|
+
let upstreamAttempts = 0;
|
|
525
|
+
const server = createServer((req, res) => {
|
|
526
|
+
void (async () => {
|
|
527
|
+
upstreamAttempts += 1;
|
|
528
|
+
await readBody(req);
|
|
529
|
+
if (upstreamAttempts === 1) {
|
|
530
|
+
req.socket.destroy();
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
res.writeHead(200, { "content-type": "text/event-stream" });
|
|
534
|
+
res.end([
|
|
535
|
+
`event: response.output_item.done\ndata: ${JSON.stringify({
|
|
536
|
+
type: "response.output_item.done",
|
|
537
|
+
output_index: 0,
|
|
538
|
+
item: {
|
|
539
|
+
type: "message",
|
|
540
|
+
id: "msg_transport_retry",
|
|
541
|
+
status: "completed",
|
|
542
|
+
role: "assistant",
|
|
543
|
+
content: [{ type: "output_text", text: "recovered", annotations: [] }]
|
|
544
|
+
}
|
|
545
|
+
})}\n\n`,
|
|
546
|
+
`event: response.completed\ndata: ${JSON.stringify({
|
|
547
|
+
response: {
|
|
548
|
+
id: "resp_transport_retry",
|
|
549
|
+
model: "gpt-5.5",
|
|
550
|
+
output: []
|
|
551
|
+
}
|
|
552
|
+
})}\n\n`
|
|
553
|
+
].join(""));
|
|
554
|
+
})().catch((error) => res.writeHead(500).end(String(error)));
|
|
555
|
+
});
|
|
556
|
+
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
557
|
+
const address = server.address();
|
|
558
|
+
assert.ok(typeof address === "object" && address !== null);
|
|
559
|
+
const accounts = await runRouteKitEffect(SubscriptionAccountSet.open(codexProvider(), {
|
|
560
|
+
source: { kind: "directory", path: directory },
|
|
561
|
+
strategy: "sticky"
|
|
562
|
+
}));
|
|
563
|
+
const relay = new CodexBackendRelay({
|
|
564
|
+
backendUrl: `http://127.0.0.1:${address.port}`,
|
|
565
|
+
catalog,
|
|
566
|
+
auth: { kind: "accounts", accounts }
|
|
567
|
+
});
|
|
568
|
+
const records = [];
|
|
569
|
+
const gateway = await startGateway({
|
|
570
|
+
backend: new RelayOnlyBackend(),
|
|
571
|
+
providerRelays: { codex: relayPorts(relay) },
|
|
572
|
+
provenance: { onModelCall: (record) => records.push(record) }
|
|
573
|
+
});
|
|
574
|
+
try {
|
|
575
|
+
const response = await fetch(`${gateway.url()}/v1/responses`, {
|
|
576
|
+
method: "POST",
|
|
577
|
+
headers: { "content-type": "application/json" },
|
|
578
|
+
body: JSON.stringify({ model: "gpt-5.5", input: "hello", stream: false })
|
|
579
|
+
});
|
|
580
|
+
assert.equal(response.status, 200);
|
|
581
|
+
assert.match(response.headers.get("content-type") ?? "", /application\/json/u);
|
|
582
|
+
const body = (await response.json());
|
|
583
|
+
assert.equal(body.id, "resp_transport_retry");
|
|
584
|
+
assert.equal(body.output?.[0]?.content?.[0]?.text, "recovered");
|
|
585
|
+
assert.equal(upstreamAttempts, 2);
|
|
586
|
+
assert.equal(records.length, 2);
|
|
587
|
+
assert.equal(records[0]?.status, "failed");
|
|
588
|
+
assert.equal(records[1]?.status, "succeeded");
|
|
589
|
+
assert.deepEqual({
|
|
590
|
+
attempts: records[1]?.metadata?.attribution?.attempts,
|
|
591
|
+
retries: records[1]?.metadata?.attribution?.retries,
|
|
592
|
+
account_failovers: records[1]?.metadata?.attribution?.account_failovers
|
|
593
|
+
}, { attempts: 2, retries: 1, account_failovers: 0 });
|
|
594
|
+
}
|
|
595
|
+
finally {
|
|
596
|
+
await Effect.runPromise(gateway.close);
|
|
597
|
+
await closeServer(server);
|
|
598
|
+
rmSync(directory, { recursive: true, force: true });
|
|
599
|
+
}
|
|
600
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { test } from "node:test";
|
|
6
|
+
import { subscriptionInfo } from "@velum-labs/routekit-registry";
|
|
7
|
+
import { runRouteKitEffect } from "@velum-labs/routekit-runtime/effect";
|
|
8
|
+
import { codexProvider } from "../subscription.js";
|
|
9
|
+
test("Codex refresh uses stored OAuth client provenance with registry fallback", async () => {
|
|
10
|
+
const root = mkdtempSync(join(tmpdir(), "routekit-codex-oauth-client-"));
|
|
11
|
+
const customPath = join(root, "custom.json");
|
|
12
|
+
const officialPath = join(root, "official.json");
|
|
13
|
+
writeFileSync(customPath, JSON.stringify({
|
|
14
|
+
tokens: {
|
|
15
|
+
access_token: "custom-access",
|
|
16
|
+
refresh_token: "custom-refresh",
|
|
17
|
+
id_token: "custom-id-token"
|
|
18
|
+
},
|
|
19
|
+
routekit: {
|
|
20
|
+
oauthClientId: "custom-codex-client",
|
|
21
|
+
retained: true
|
|
22
|
+
}
|
|
23
|
+
}));
|
|
24
|
+
writeFileSync(officialPath, JSON.stringify({
|
|
25
|
+
tokens: {
|
|
26
|
+
access_token: "official-access",
|
|
27
|
+
refresh_token: "official-refresh"
|
|
28
|
+
}
|
|
29
|
+
}));
|
|
30
|
+
const originalFetch = globalThis.fetch;
|
|
31
|
+
const clientIds = [];
|
|
32
|
+
globalThis.fetch = async (input, init) => {
|
|
33
|
+
assert.equal(String(input), subscriptionInfo("codex").oauth.tokenEndpoint);
|
|
34
|
+
const clientId = new URLSearchParams(String(init?.body)).get("client_id");
|
|
35
|
+
assert.ok(clientId);
|
|
36
|
+
clientIds.push(clientId);
|
|
37
|
+
return Response.json({
|
|
38
|
+
access_token: `rotated-access-${clientIds.length}`,
|
|
39
|
+
refresh_token: `rotated-refresh-${clientIds.length}`,
|
|
40
|
+
expires_in: 3600
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
try {
|
|
44
|
+
const provider = codexProvider();
|
|
45
|
+
const custom = await runRouteKitEffect(provider.loadCredential(customPath));
|
|
46
|
+
assert.equal(custom.oauthClientId, "custom-codex-client");
|
|
47
|
+
const refreshedCustom = await runRouteKitEffect(provider.refresh(custom));
|
|
48
|
+
assert.equal(refreshedCustom.oauthClientId, "custom-codex-client");
|
|
49
|
+
const persist = provider.persistCredential;
|
|
50
|
+
assert.ok(persist);
|
|
51
|
+
const persistedCustom = await runRouteKitEffect(persist(custom, refreshedCustom));
|
|
52
|
+
assert.equal(persistedCustom.oauthClientId, "custom-codex-client");
|
|
53
|
+
const persistedBlob = JSON.parse(readFileSync(customPath, "utf8"));
|
|
54
|
+
assert.equal(persistedBlob.tokens?.id_token, "custom-id-token");
|
|
55
|
+
assert.equal(persistedBlob.routekit?.oauthClientId, "custom-codex-client");
|
|
56
|
+
assert.equal(persistedBlob.routekit?.retained, true);
|
|
57
|
+
const official = await runRouteKitEffect(provider.loadCredential(officialPath));
|
|
58
|
+
assert.equal(official.oauthClientId, undefined);
|
|
59
|
+
const refreshedOfficial = await runRouteKitEffect(provider.refresh(official));
|
|
60
|
+
assert.equal(refreshedOfficial.oauthClientId, undefined);
|
|
61
|
+
assert.deepEqual(clientIds, [
|
|
62
|
+
"custom-codex-client",
|
|
63
|
+
subscriptionInfo("codex").oauth.clientId
|
|
64
|
+
]);
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
globalThis.fetch = originalFetch;
|
|
68
|
+
rmSync(root, { recursive: true, force: true });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|