@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,108 @@
|
|
|
1
|
+
import { hashCanonicalSha256 } from "@velum-labs/routekit-contracts";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
const OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
|
|
5
|
+
class OpenRouterPricingDiscoveryError extends Error {
|
|
6
|
+
_tag = "OpenRouterPricingDiscoveryError";
|
|
7
|
+
}
|
|
8
|
+
const decimal = z.union([z.string(), z.number()]);
|
|
9
|
+
const pricingSchema = z
|
|
10
|
+
.object({
|
|
11
|
+
prompt: decimal,
|
|
12
|
+
completion: decimal,
|
|
13
|
+
input_cache_read: decimal.optional(),
|
|
14
|
+
input_cache_write: decimal.optional(),
|
|
15
|
+
overrides: z
|
|
16
|
+
.array(z
|
|
17
|
+
.object({
|
|
18
|
+
min_prompt_tokens: z.number().int().nonnegative(),
|
|
19
|
+
prompt: decimal,
|
|
20
|
+
completion: decimal,
|
|
21
|
+
input_cache_read: decimal.optional(),
|
|
22
|
+
input_cache_write: decimal.optional()
|
|
23
|
+
})
|
|
24
|
+
.passthrough())
|
|
25
|
+
.optional()
|
|
26
|
+
})
|
|
27
|
+
.passthrough();
|
|
28
|
+
const responseSchema = z
|
|
29
|
+
.object({
|
|
30
|
+
data: z.array(z
|
|
31
|
+
.object({
|
|
32
|
+
id: z.string().min(1),
|
|
33
|
+
pricing: pricingSchema
|
|
34
|
+
})
|
|
35
|
+
.passthrough())
|
|
36
|
+
})
|
|
37
|
+
.passthrough();
|
|
38
|
+
function perMillion(value) {
|
|
39
|
+
const parsed = typeof value === "number" ? value : Number(value);
|
|
40
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
41
|
+
throw new Error("OpenRouter returned an invalid token price.");
|
|
42
|
+
}
|
|
43
|
+
return parsed * 1_000_000;
|
|
44
|
+
}
|
|
45
|
+
function rates(pricing) {
|
|
46
|
+
return {
|
|
47
|
+
inputPer1mTokens: perMillion(pricing.prompt),
|
|
48
|
+
...(pricing.input_cache_read === undefined
|
|
49
|
+
? {}
|
|
50
|
+
: { cachedInputReadPer1mTokens: perMillion(pricing.input_cache_read) }),
|
|
51
|
+
...(pricing.input_cache_write === undefined
|
|
52
|
+
? {}
|
|
53
|
+
: { cachedInputWritePer1mTokens: perMillion(pricing.input_cache_write) }),
|
|
54
|
+
outputPer1mTokens: perMillion(pricing.completion)
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export function decodeOpenRouterPricing(value, retrievedAt) {
|
|
58
|
+
const parsed = responseSchema.parse(value);
|
|
59
|
+
const revision = hashCanonicalSha256(parsed.data.map(({ id, pricing }) => ({ id, pricing })));
|
|
60
|
+
return parsed.data.map(({ id, pricing }) => ({
|
|
61
|
+
key: `openrouter/${id}`,
|
|
62
|
+
subject: { provider: "openrouter", nativeModel: id },
|
|
63
|
+
currency: "USD",
|
|
64
|
+
rates: rates(pricing),
|
|
65
|
+
...(pricing.overrides === undefined
|
|
66
|
+
? {}
|
|
67
|
+
: {
|
|
68
|
+
overrides: pricing.overrides.map((override) => ({
|
|
69
|
+
minimumInputTokens: override.min_prompt_tokens,
|
|
70
|
+
rates: rates(override)
|
|
71
|
+
}))
|
|
72
|
+
}),
|
|
73
|
+
evidence: {
|
|
74
|
+
kind: "provider-catalog",
|
|
75
|
+
source: OPENROUTER_MODELS_URL,
|
|
76
|
+
revision,
|
|
77
|
+
retrievedAt
|
|
78
|
+
}
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
81
|
+
export const openRouterPricingAdapter = {
|
|
82
|
+
source: OPENROUTER_MODELS_URL,
|
|
83
|
+
discover: ({ transport, retrievedAt, signal }) => Effect.gen(function* () {
|
|
84
|
+
const response = yield* transport(OPENROUTER_MODELS_URL, {
|
|
85
|
+
method: "GET",
|
|
86
|
+
headers: { accept: "application/json", "x-title": "RouteKit" },
|
|
87
|
+
...(signal === undefined ? {} : { signal })
|
|
88
|
+
}, { responseMode: "buffered" });
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
return yield* Effect.fail(new OpenRouterPricingDiscoveryError(`OpenRouter pricing discovery failed with HTTP ${response.status}.`));
|
|
91
|
+
}
|
|
92
|
+
const contentLength = Number(response.headers.get("content-length"));
|
|
93
|
+
if (Number.isFinite(contentLength) && contentLength > 8 * 1024 * 1024) {
|
|
94
|
+
return yield* Effect.fail(new OpenRouterPricingDiscoveryError("OpenRouter pricing catalog exceeds the size limit."));
|
|
95
|
+
}
|
|
96
|
+
const text = yield* Effect.tryPromise({
|
|
97
|
+
try: () => response.text(),
|
|
98
|
+
catch: () => new OpenRouterPricingDiscoveryError("OpenRouter pricing catalog could not be read.")
|
|
99
|
+
});
|
|
100
|
+
if (Buffer.byteLength(text, "utf8") > 8 * 1024 * 1024) {
|
|
101
|
+
return yield* Effect.fail(new OpenRouterPricingDiscoveryError("OpenRouter pricing catalog exceeds the size limit."));
|
|
102
|
+
}
|
|
103
|
+
return yield* Effect.try({
|
|
104
|
+
try: () => decodeOpenRouterPricing(JSON.parse(text), retrievedAt),
|
|
105
|
+
catch: () => new OpenRouterPricingDiscoveryError("OpenRouter pricing catalog is invalid.")
|
|
106
|
+
});
|
|
107
|
+
})
|
|
108
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { calculatePKCECodeChallenge } from "oauth4webapi";
|
|
4
|
+
import { runRouteKitEffect } from "@velum-labs/routekit-runtime/effect";
|
|
5
|
+
import { openRouterCredentials, openRouterEnrollment } from "../connection.js";
|
|
6
|
+
test("official OpenRouter SDK performs S256 code-to-key exchange without exposing the key in source config", async () => {
|
|
7
|
+
let calls = 0;
|
|
8
|
+
const context = {
|
|
9
|
+
enrollmentId: "connection", sourceId: "production", nonce: "nonce",
|
|
10
|
+
settings: {}, callbackUrl: "https://cloud.example/callback?state=nonce",
|
|
11
|
+
fetch: async (input, init) => {
|
|
12
|
+
calls += 1;
|
|
13
|
+
const request = new Request(input, init);
|
|
14
|
+
assert.equal(request.url, "https://openrouter.ai/api/v1/auth/keys");
|
|
15
|
+
const body = await request.json();
|
|
16
|
+
assert.equal(body.code, "authorization-code");
|
|
17
|
+
assert.equal(body.code_challenge_method, "S256");
|
|
18
|
+
assert.equal(await calculatePKCECodeChallenge(body.code_verifier), challenge);
|
|
19
|
+
return Response.json({ key: "secret-generated-key", user_id: null });
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
const started = await runRouteKitEffect(openRouterEnrollment.start(context, {}));
|
|
23
|
+
assert.equal(started.kind, "action");
|
|
24
|
+
if (started.kind !== "action" || started.action.kind !== "external")
|
|
25
|
+
throw new Error("Missing external action");
|
|
26
|
+
const authorize = new URL(started.action.url);
|
|
27
|
+
const challenge = authorize.searchParams.get("code_challenge");
|
|
28
|
+
assert.equal(authorize.searchParams.get("code_challenge_method"), "S256");
|
|
29
|
+
assert.equal(authorize.searchParams.get("callback_url"), context.callbackUrl);
|
|
30
|
+
assert.doesNotMatch(JSON.stringify(started.action), /verifier/);
|
|
31
|
+
const completed = await runRouteKitEffect(openRouterEnrollment.resume(context, started.state, {
|
|
32
|
+
kind: "callback", parameters: { code: "authorization-code" }
|
|
33
|
+
}));
|
|
34
|
+
assert.equal(completed.kind, "complete");
|
|
35
|
+
if (completed.kind !== "complete")
|
|
36
|
+
throw new Error("Missing connection");
|
|
37
|
+
assert.doesNotMatch(JSON.stringify(completed.connection.config), /secret-generated-key/);
|
|
38
|
+
const runtime = await runRouteKitEffect(openRouterCredentials.resolve(completed.connection.credential, { sourceId: "production" }));
|
|
39
|
+
assert.equal(runtime.environment?.ROUTEKIT_CLOUD_GRANT_OPENROUTER_KEY, "secret-generated-key");
|
|
40
|
+
assert.equal(calls, 1);
|
|
41
|
+
});
|
|
42
|
+
test("denial and unknown enrollment state versions do not contact OpenRouter", async () => {
|
|
43
|
+
const context = {
|
|
44
|
+
enrollmentId: "connection", sourceId: "production", nonce: "nonce", settings: {},
|
|
45
|
+
callbackUrl: "https://cloud.example/callback",
|
|
46
|
+
fetch: async () => { assert.fail("Must not exchange"); }
|
|
47
|
+
};
|
|
48
|
+
await assert.rejects(runRouteKitEffect(openRouterEnrollment.resume(context, { version: 2, data: {} }, { kind: "callback", parameters: { code: "x" } })));
|
|
49
|
+
await assert.rejects(runRouteKitEffect(openRouterEnrollment.resume(context, { version: 1, data: { verifier: "v".repeat(43) } }, { kind: "callback", parameters: { error: "access_denied" } })));
|
|
50
|
+
});
|
|
51
|
+
test("an ambiguous OpenRouter exchange is never retried and does not expose upstream details", async () => {
|
|
52
|
+
for (const failure of ["http", "network"]) {
|
|
53
|
+
let calls = 0;
|
|
54
|
+
const context = {
|
|
55
|
+
enrollmentId: "connection", sourceId: "production", nonce: "nonce", settings: {},
|
|
56
|
+
callbackUrl: "https://cloud.example/callback",
|
|
57
|
+
fetch: async () => {
|
|
58
|
+
calls += 1;
|
|
59
|
+
if (failure === "network")
|
|
60
|
+
throw new Error("private-upstream-detail");
|
|
61
|
+
return Response.json({ error: { message: "private-upstream-detail", code: 500 } }, { status: 500 });
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
await assert.rejects(runRouteKitEffect(openRouterEnrollment.resume(context, { version: 1, data: { verifier: "v".repeat(43) } }, { kind: "callback", parameters: { code: "possibly-consumed-code" } })), (error) => {
|
|
65
|
+
assert.match(String(error), /authorization could not be completed/);
|
|
66
|
+
assert.doesNotMatch(String(error), /private-upstream-detail|possibly-consumed-code/);
|
|
67
|
+
return true;
|
|
68
|
+
});
|
|
69
|
+
assert.equal(calls, 1);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { decodeOpenRouterPricing } from "../pricing.js";
|
|
4
|
+
test("decodes OpenRouter token prices and long-prompt overrides", () => {
|
|
5
|
+
const quotes = decodeOpenRouterPricing({
|
|
6
|
+
data: [
|
|
7
|
+
{
|
|
8
|
+
id: "openai/gpt-6-astra",
|
|
9
|
+
pricing: {
|
|
10
|
+
prompt: "0.00001",
|
|
11
|
+
completion: "0.00005",
|
|
12
|
+
input_cache_read: "0.000001",
|
|
13
|
+
input_cache_write: "0.0000125",
|
|
14
|
+
overrides: [
|
|
15
|
+
{
|
|
16
|
+
min_prompt_tokens: 272000,
|
|
17
|
+
prompt: "0.00002",
|
|
18
|
+
completion: "0.000075",
|
|
19
|
+
input_cache_read: "0.000002",
|
|
20
|
+
input_cache_write: "0.000025"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}, "2026-09-08T00:00:00.000Z");
|
|
27
|
+
assert.equal(quotes.length, 1);
|
|
28
|
+
assert.deepEqual(quotes[0]?.subject, {
|
|
29
|
+
provider: "openrouter",
|
|
30
|
+
nativeModel: "openai/gpt-6-astra"
|
|
31
|
+
});
|
|
32
|
+
assert.deepEqual(quotes[0]?.rates, {
|
|
33
|
+
inputPer1mTokens: 10,
|
|
34
|
+
cachedInputReadPer1mTokens: 1,
|
|
35
|
+
cachedInputWritePer1mTokens: 12.5,
|
|
36
|
+
outputPer1mTokens: 50
|
|
37
|
+
});
|
|
38
|
+
assert.deepEqual(quotes[0]?.overrides?.[0]?.rates, {
|
|
39
|
+
inputPer1mTokens: 20,
|
|
40
|
+
cachedInputReadPer1mTokens: 2,
|
|
41
|
+
cachedInputWritePer1mTokens: 25,
|
|
42
|
+
outputPer1mTokens: 75
|
|
43
|
+
});
|
|
44
|
+
assert.match(quotes[0]?.evidence.revision ?? "", /^sha256:[a-f0-9]{64}$/);
|
|
45
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { codexCompatibility } from "@velum-labs/routekit-contracts";
|
|
4
|
+
import { attachAnthropicRequestMetadata, attachReasoningSelection, createConnectedProviderSource } from "@velum-labs/routekit-provider-core";
|
|
5
|
+
import { runRouteKitEffect } from "@velum-labs/routekit-runtime/effect";
|
|
6
|
+
import { Effect } from "effect";
|
|
7
|
+
import { openrouter } from "../index.js";
|
|
8
|
+
test("OpenRouter flow resolves its stored key before the authenticated identity probe", async () => {
|
|
9
|
+
let calls = 0;
|
|
10
|
+
const source = await runRouteKitEffect(createConnectedProviderSource(openrouter, {
|
|
11
|
+
sourceId: "oauth-connection",
|
|
12
|
+
config: {
|
|
13
|
+
use: "openrouter",
|
|
14
|
+
auth: { kind: "env-bearer", env: "ROUTEKIT_CLOUD_GRANT_OPENROUTER_KEY" }
|
|
15
|
+
},
|
|
16
|
+
env: {},
|
|
17
|
+
resources: {
|
|
18
|
+
transport: (url, init) => {
|
|
19
|
+
calls++;
|
|
20
|
+
assert.equal(url, "https://openrouter.ai/api/v1/key");
|
|
21
|
+
assert.equal(new Headers(init.headers).get("authorization"), "Bearer stored-flow-key");
|
|
22
|
+
assert.equal(init.redirect, "error");
|
|
23
|
+
return Effect.succeed(Response.json({ data: { label: "RouteKit" } }));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}, { version: 1, kind: "api-key", payload: { key: "stored-flow-key" } }, { validate: true }));
|
|
27
|
+
assert.equal(source.sourceId, "oauth-connection");
|
|
28
|
+
assert.equal(calls, 1);
|
|
29
|
+
});
|
|
30
|
+
test("configured OpenRouter IDs stay fail closed without provider capability metadata", async () => {
|
|
31
|
+
const prepared = openrouter.connection.prepare({
|
|
32
|
+
sourceId: "openrouter-production",
|
|
33
|
+
methodId: "api-key",
|
|
34
|
+
values: {
|
|
35
|
+
"api-key": "openrouter-secret",
|
|
36
|
+
models: ["anthropic/claude-sonnet"]
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
const source = await runRouteKitEffect(openrouter.createSource({
|
|
40
|
+
sourceId: "openrouter-production",
|
|
41
|
+
config: prepared.config,
|
|
42
|
+
env: prepared.environment
|
|
43
|
+
}));
|
|
44
|
+
const [model] = await runRouteKitEffect(source.discovery.discoverModels());
|
|
45
|
+
assert.deepEqual(model, { id: "anthropic/claude-sonnet" });
|
|
46
|
+
assert.equal(codexCompatibility({
|
|
47
|
+
id: `openrouter-production/${model.id}`,
|
|
48
|
+
provider: "openrouter",
|
|
49
|
+
architecture: model?.metadata?.architecture,
|
|
50
|
+
supportedParameters: model?.metadata?.supportedParameters,
|
|
51
|
+
reasoning: model?.reasoning
|
|
52
|
+
}).status, "incompatible");
|
|
53
|
+
});
|
|
54
|
+
test("OpenRouter discovery owns the reasoning efforts for its routed models", async () => {
|
|
55
|
+
const prepared = openrouter.connection.prepare({
|
|
56
|
+
sourceId: "openrouter-production",
|
|
57
|
+
methodId: "api-key",
|
|
58
|
+
values: { "api-key": "openrouter-secret" }
|
|
59
|
+
});
|
|
60
|
+
const requests = [];
|
|
61
|
+
const source = await runRouteKitEffect(openrouter.createSource({
|
|
62
|
+
sourceId: "openrouter-production",
|
|
63
|
+
config: prepared.config,
|
|
64
|
+
env: prepared.environment,
|
|
65
|
+
resources: {
|
|
66
|
+
transport: (url) => {
|
|
67
|
+
requests.push(url);
|
|
68
|
+
return Effect.succeed(Response.json({
|
|
69
|
+
data: [
|
|
70
|
+
{
|
|
71
|
+
id: "openai/gpt-5.6-sol",
|
|
72
|
+
supported_parameters: ["reasoning", "reasoning_effort", "tools"],
|
|
73
|
+
reasoning: {
|
|
74
|
+
mandatory: false,
|
|
75
|
+
default_enabled: true,
|
|
76
|
+
supported_efforts: ["max", "xhigh", "high", "medium", "low", "none"],
|
|
77
|
+
default_effort: "medium"
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
{ id: "openai/private-model" }
|
|
81
|
+
]
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}));
|
|
86
|
+
const models = await runRouteKitEffect(source.discovery.discoverModels());
|
|
87
|
+
assert.deepEqual(requests, ["https://openrouter.ai/api/v1/models"]);
|
|
88
|
+
assert.equal(models[0]?.reasoning?.status, "supported");
|
|
89
|
+
assert.equal(models[0]?.reasoning?.wireShape, "openrouter");
|
|
90
|
+
assert.equal(models[0]?.reasoning?.defaultEffort, "medium");
|
|
91
|
+
assert.deepEqual(models[0]?.reasoning?.efforts?.map((effort) => effort.id), [
|
|
92
|
+
"max", "xhigh", "high", "medium", "low", "none"
|
|
93
|
+
]);
|
|
94
|
+
assert.equal(models[0]?.reasoning?.provenance, "provider");
|
|
95
|
+
assert.equal(models[1]?.reasoning, undefined);
|
|
96
|
+
});
|
|
97
|
+
test("OpenRouter egress projects native Claude reasoning as enabled or token budget", async () => {
|
|
98
|
+
const prepared = openrouter.connection.prepare({
|
|
99
|
+
sourceId: "openrouter-production",
|
|
100
|
+
methodId: "api-key",
|
|
101
|
+
values: { "api-key": "openrouter-secret", models: ["anthropic/claude-haiku-4.5"] }
|
|
102
|
+
});
|
|
103
|
+
const payloads = [];
|
|
104
|
+
const source = await runRouteKitEffect(openrouter.createSource({
|
|
105
|
+
sourceId: "openrouter-production",
|
|
106
|
+
config: prepared.config,
|
|
107
|
+
env: prepared.environment,
|
|
108
|
+
resources: {
|
|
109
|
+
transport: (_url, init) => {
|
|
110
|
+
payloads.push(JSON.parse(String(init.body)));
|
|
111
|
+
return Effect.succeed(Response.json({ choices: [] }));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}));
|
|
115
|
+
const capability = {
|
|
116
|
+
status: "supported",
|
|
117
|
+
adaptive: true,
|
|
118
|
+
budget: { minTokens: 1_024 },
|
|
119
|
+
wireShape: "openrouter",
|
|
120
|
+
provenance: "provider"
|
|
121
|
+
};
|
|
122
|
+
for (const selection of [
|
|
123
|
+
{ mode: "adaptive" },
|
|
124
|
+
{ mode: "budget", budgetTokens: 2048 }
|
|
125
|
+
]) {
|
|
126
|
+
const body = {
|
|
127
|
+
model: "anthropic/claude-haiku-4.5",
|
|
128
|
+
messages: [{ role: "user", content: "hi" }],
|
|
129
|
+
reasoning_effort: "high"
|
|
130
|
+
};
|
|
131
|
+
attachReasoningSelection(body, selection);
|
|
132
|
+
attachAnthropicRequestMetadata(body, {
|
|
133
|
+
thinking: selection.mode === "adaptive"
|
|
134
|
+
? { type: "adaptive" }
|
|
135
|
+
: { type: "enabled", budget_tokens: selection.budgetTokens }
|
|
136
|
+
});
|
|
137
|
+
await runRouteKitEffect(source.requests.chat(body, undefined, { reasoningCapabilities: capability }));
|
|
138
|
+
}
|
|
139
|
+
assert.deepEqual(payloads.map((body) => body.reasoning), [{ enabled: true }, { max_tokens: 2048 }]);
|
|
140
|
+
assert.ok(payloads.every((body) => body.reasoning_effort === undefined));
|
|
141
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createConnectedProviderSource } from "@velum-labs/routekit-provider-core";
|
|
4
|
+
import { RouteKitFailure, runRouteKitEffect } from "@velum-labs/routekit-runtime/effect";
|
|
5
|
+
import { Effect, Fiber } from "effect";
|
|
6
|
+
import { TestClock } from "effect/testing";
|
|
7
|
+
import { openrouter } from "../index.js";
|
|
8
|
+
const key = "private-openrouter-key";
|
|
9
|
+
const currentKey = { data: { label: "RouteKit", limit: 0, limit_remaining: 0, usage: 42 } };
|
|
10
|
+
const modelId = "vendor/private-model";
|
|
11
|
+
function context(transport, configured = false) {
|
|
12
|
+
return {
|
|
13
|
+
sourceId: "team-router",
|
|
14
|
+
config: {
|
|
15
|
+
use: "openrouter",
|
|
16
|
+
auth: { kind: "env-bearer", env: "TEAM_ROUTER_KEY" },
|
|
17
|
+
...(configured ? { options: { models: [modelId] } } : {})
|
|
18
|
+
},
|
|
19
|
+
env: { TEAM_ROUTER_KEY: key },
|
|
20
|
+
resources: { transport }
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function validate(input) {
|
|
24
|
+
assert.ok(openrouter.connection.validate, "OpenRouter owns its connection validation hook");
|
|
25
|
+
return openrouter.connection.validate(input);
|
|
26
|
+
}
|
|
27
|
+
function safeFailure(error) {
|
|
28
|
+
assert.doesNotMatch(String(error) + JSON.stringify(error), /private-openrouter-key|upstream-secret/);
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
test("pasted and stored browser keys are probed once before either discovery or configured IDs", async () => {
|
|
32
|
+
for (const stored of [false, true]) {
|
|
33
|
+
for (const configured of [false, true]) {
|
|
34
|
+
const requests = [];
|
|
35
|
+
const input = context((url, init) => {
|
|
36
|
+
requests.push(url);
|
|
37
|
+
assert.equal(new Headers(init.headers).get("authorization"), `Bearer ${key}`);
|
|
38
|
+
if (url.endsWith("/key")) {
|
|
39
|
+
assert.equal(init.method, "GET");
|
|
40
|
+
assert.equal(init.redirect, "error");
|
|
41
|
+
assert.equal(init.body, undefined);
|
|
42
|
+
return Effect.succeed(Response.json(currentKey));
|
|
43
|
+
}
|
|
44
|
+
assert.equal(url, "https://openrouter.ai/api/v1/models");
|
|
45
|
+
return Effect.succeed(Response.json({ data: [{ id: modelId }] }));
|
|
46
|
+
}, configured);
|
|
47
|
+
if (stored) {
|
|
48
|
+
input.config = {
|
|
49
|
+
...input.config,
|
|
50
|
+
auth: { kind: "env-bearer", env: "ROUTEKIT_CLOUD_GRANT_OPENROUTER_KEY" }
|
|
51
|
+
};
|
|
52
|
+
input.env = {};
|
|
53
|
+
}
|
|
54
|
+
const source = await runRouteKitEffect(createConnectedProviderSource(openrouter, input, stored ? { version: 1, kind: "api-key", payload: { key } } : undefined, { validate: true }));
|
|
55
|
+
assert.deepEqual(requests, ["https://openrouter.ai/api/v1/key"]);
|
|
56
|
+
assert.equal(source.sourceId, "team-router");
|
|
57
|
+
assert.deepEqual((await runRouteKitEffect(source.discovery.discoverModels())).map((m) => m.id), [modelId]);
|
|
58
|
+
assert.deepEqual(requests, configured
|
|
59
|
+
? ["https://openrouter.ai/api/v1/key"]
|
|
60
|
+
: ["https://openrouter.ai/api/v1/key", "https://openrouter.ai/api/v1/models"]);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
test("the hook preserves canonical and custom API prefixes without duplicating api or v1", async () => {
|
|
65
|
+
for (const [baseUrl, expected] of [
|
|
66
|
+
[undefined, "https://openrouter.ai/api/v1/key"],
|
|
67
|
+
["https://openrouter.ai/api", "https://openrouter.ai/api/v1/key"],
|
|
68
|
+
["https://openrouter.ai/api/v1/", "https://openrouter.ai/api/v1/key"],
|
|
69
|
+
["https://proxy.example", "https://proxy.example/v1/key"],
|
|
70
|
+
["https://proxy.example/v1", "https://proxy.example/v1/key"],
|
|
71
|
+
["https://proxy.example/team/api/", "https://proxy.example/team/api/v1/key"],
|
|
72
|
+
["https://proxy.example/team/api/v1/", "https://proxy.example/team/api/v1/key"]
|
|
73
|
+
]) {
|
|
74
|
+
let calls = 0;
|
|
75
|
+
const input = context((url, init) => {
|
|
76
|
+
calls++;
|
|
77
|
+
assert.equal(url, expected);
|
|
78
|
+
assert.equal(init.redirect, "error");
|
|
79
|
+
assert.equal(new Headers(init.headers).get("authorization"), `Bearer ${key}`);
|
|
80
|
+
return Effect.succeed(Response.json(currentKey));
|
|
81
|
+
});
|
|
82
|
+
input.config = { ...input.config, ...(baseUrl === undefined ? {} : { baseUrl }) };
|
|
83
|
+
await runRouteKitEffect(validate(input));
|
|
84
|
+
assert.equal(calls, 1);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
test("registry environment and custom authentication headers match source authentication", async () => {
|
|
88
|
+
for (const customHeader of [false, true]) {
|
|
89
|
+
const input = context((_url, init) => {
|
|
90
|
+
const headers = new Headers(init.headers);
|
|
91
|
+
assert.equal(headers.get(customHeader ? "x-proxy-key" : "authorization"), customHeader ? key : `Bearer ${key}`);
|
|
92
|
+
if (customHeader)
|
|
93
|
+
assert.equal(headers.get("authorization"), null);
|
|
94
|
+
return Effect.succeed(Response.json(currentKey));
|
|
95
|
+
});
|
|
96
|
+
input.config = {
|
|
97
|
+
use: "openrouter",
|
|
98
|
+
...(customHeader ? {
|
|
99
|
+
baseUrl: "https://proxy.example/api",
|
|
100
|
+
auth: { kind: "env-header", env: "OPENROUTER_API_KEY", header: "x-proxy-key" }
|
|
101
|
+
} : {})
|
|
102
|
+
};
|
|
103
|
+
input.env = { OPENROUTER_API_KEY: key };
|
|
104
|
+
await runRouteKitEffect(validate(input));
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
test("zero, exhausted, negative remaining, and unlimited allowances still identify a valid key", async () => {
|
|
108
|
+
for (const allowance of [
|
|
109
|
+
{ limit: 0, limit_remaining: 0 },
|
|
110
|
+
{ limit: 10, limit_remaining: 0, usage: 10 },
|
|
111
|
+
{ limit: 10, limit_remaining: -1, usage: 11 },
|
|
112
|
+
{ limit: null, limit_remaining: null }
|
|
113
|
+
]) {
|
|
114
|
+
await runRouteKitEffect(validate(context(() => Effect.succeed(Response.json({ data: { label: "RouteKit", ...allowance } })))));
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
test("HTTP rejections fail before discovery and discard rather than expose upstream bodies", async () => {
|
|
118
|
+
for (const configured of [false, true]) {
|
|
119
|
+
for (const status of [401, 403, 402, 429, 500, 503, 307]) {
|
|
120
|
+
let calls = 0;
|
|
121
|
+
let cancelled = false;
|
|
122
|
+
const input = context((url) => {
|
|
123
|
+
calls++;
|
|
124
|
+
assert.equal(url, "https://openrouter.ai/api/v1/key");
|
|
125
|
+
return Effect.succeed(new Response(new ReadableStream({
|
|
126
|
+
start(controller) { controller.enqueue(new TextEncoder().encode("upstream-secret")); },
|
|
127
|
+
cancel() { cancelled = true; }
|
|
128
|
+
}), { status, headers: { location: "https://unintended.example" } }));
|
|
129
|
+
}, configured);
|
|
130
|
+
await assert.rejects(runRouteKitEffect(createConnectedProviderSource(openrouter, input, undefined, { validate: true })), (error) => {
|
|
131
|
+
assert.match(String(error), new RegExp(`HTTP ${status} at /v1/key`));
|
|
132
|
+
return safeFailure(error);
|
|
133
|
+
});
|
|
134
|
+
assert.equal(calls, 1);
|
|
135
|
+
assert.equal(cancelled, true);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
test("malformed JSON, public catalogs, empty bodies and oversized responses cannot validate", async () => {
|
|
140
|
+
for (const body of [
|
|
141
|
+
"upstream-secret", "null", "[]", "{}", '{"data":[]}',
|
|
142
|
+
'{"data":{"label":123}}', '{"data":[{"id":"public-model"}]}',
|
|
143
|
+
JSON.stringify({ data: { label: "upstream-secret".repeat(6_000) } })
|
|
144
|
+
]) {
|
|
145
|
+
await assert.rejects(runRouteKitEffect(validate(context(() => Effect.succeed(new Response(body))))), safeFailure);
|
|
146
|
+
}
|
|
147
|
+
await assert.rejects(runRouteKitEffect(validate(context(() => Effect.succeed(new Response(null, { status: 204 }))))), safeFailure);
|
|
148
|
+
});
|
|
149
|
+
test("missing credentials and transport failures are sanitized and never retried", async () => {
|
|
150
|
+
const missing = context(() => { assert.fail("Missing credentials must not leave the process"); });
|
|
151
|
+
missing.env = {};
|
|
152
|
+
await assert.rejects(runRouteKitEffect(validate(missing)), safeFailure);
|
|
153
|
+
let calls = 0;
|
|
154
|
+
await assert.rejects(runRouteKitEffect(validate(context(() => {
|
|
155
|
+
calls++;
|
|
156
|
+
return Effect.fail(new RouteKitFailure({ message: `upstream-secret ${key}` }));
|
|
157
|
+
}))), safeFailure);
|
|
158
|
+
assert.equal(calls, 1);
|
|
159
|
+
});
|
|
160
|
+
test("streamed identity JSON handles split UTF-8 and cancels bodies that exceed the size bound", async () => {
|
|
161
|
+
const bytes = new TextEncoder().encode(JSON.stringify({ data: { label: "RouteKit 🔑" } }));
|
|
162
|
+
await runRouteKitEffect(validate(context(() => Effect.succeed(new Response(new ReadableStream({
|
|
163
|
+
start(controller) {
|
|
164
|
+
for (const byte of bytes)
|
|
165
|
+
controller.enqueue(Uint8Array.of(byte));
|
|
166
|
+
controller.close();
|
|
167
|
+
}
|
|
168
|
+
}))))));
|
|
169
|
+
let pulls = 0;
|
|
170
|
+
let cancelled = false;
|
|
171
|
+
await assert.rejects(runRouteKitEffect(validate(context(() => Effect.succeed(new Response(new ReadableStream({
|
|
172
|
+
pull(controller) {
|
|
173
|
+
pulls++;
|
|
174
|
+
controller.enqueue(new Uint8Array(4 * 1024));
|
|
175
|
+
},
|
|
176
|
+
cancel() { cancelled = true; }
|
|
177
|
+
})))))), /invalid current-key response/);
|
|
178
|
+
assert.ok(pulls <= 18, `Read ${pulls} chunks from an unbounded response`);
|
|
179
|
+
assert.equal(cancelled, true);
|
|
180
|
+
});
|
|
181
|
+
test("a stalled cleanup cannot hide or indefinitely delay the sanitized HTTP failure", { timeout: 2_000 }, async () => {
|
|
182
|
+
let cancelled = false;
|
|
183
|
+
await assert.rejects(runRouteKitEffect(validate(context(() => Effect.succeed(new Response(new ReadableStream({
|
|
184
|
+
cancel() {
|
|
185
|
+
cancelled = true;
|
|
186
|
+
return new Promise(() => { });
|
|
187
|
+
}
|
|
188
|
+
}), { status: 401 }))))), /HTTP 401 at \/v1\/key/);
|
|
189
|
+
assert.equal(cancelled, true);
|
|
190
|
+
});
|
|
191
|
+
test("the hook bounds stalled headers and bodies, cancelling owned work at its deadline", async () => {
|
|
192
|
+
for (const stage of ["headers", "body"]) {
|
|
193
|
+
let cancelled = false;
|
|
194
|
+
let markStarted;
|
|
195
|
+
const started = new Promise((resolve) => { markStarted = resolve; });
|
|
196
|
+
const transport = stage === "headers"
|
|
197
|
+
? () => Effect.tryPromise({
|
|
198
|
+
try: (signal) => new Promise(() => {
|
|
199
|
+
signal.addEventListener("abort", () => { cancelled = true; }, { once: true });
|
|
200
|
+
markStarted();
|
|
201
|
+
}),
|
|
202
|
+
catch: () => new RouteKitFailure({ message: "upstream-secret" })
|
|
203
|
+
})
|
|
204
|
+
: () => Effect.succeed(new Response(new ReadableStream({
|
|
205
|
+
pull() { markStarted(); return new Promise(() => { }); },
|
|
206
|
+
cancel() { cancelled = true; }
|
|
207
|
+
}, { highWaterMark: 0 })));
|
|
208
|
+
const result = await runRouteKitEffect(Effect.gen(function* () {
|
|
209
|
+
const fiber = yield* validate(context(transport)).pipe(
|
|
210
|
+
// Cloud validates while acquiring a source; the hook must remain interruptible.
|
|
211
|
+
Effect.uninterruptible, Effect.catch((error) => Effect.succeed(error)), Effect.forkChild);
|
|
212
|
+
yield* Effect.promise(() => started);
|
|
213
|
+
yield* TestClock.adjust(10_001);
|
|
214
|
+
return yield* Fiber.join(fiber);
|
|
215
|
+
}).pipe(Effect.provide(TestClock.layer())));
|
|
216
|
+
assert.match(String(result), /exceeded its deadline/);
|
|
217
|
+
assert.equal(cancelled, true, stage);
|
|
218
|
+
safeFailure(result);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
test("source creation without host validation does not introduce a probe or inference", async () => {
|
|
222
|
+
await runRouteKitEffect(createConnectedProviderSource(openrouter, context(() => {
|
|
223
|
+
assert.fail("Unvalidated source creation must not use transport");
|
|
224
|
+
}, true)));
|
|
225
|
+
});
|
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import type { EffectRouteKitControlHandlers } from "@velum-labs/routekit-control/effect";
|
|
2
2
|
type LauncherHandlers = Pick<EffectRouteKitControlHandlers, "launcher.prepare">;
|
|
3
|
-
export type
|
|
3
|
+
export type LauncherHandlerDependencies = {
|
|
4
4
|
listModels: EffectRouteKitControlHandlers["models.list"];
|
|
5
5
|
};
|
|
6
|
-
/**
|
|
7
|
-
export declare
|
|
8
|
-
private readonly options;
|
|
9
|
-
constructor(options: LauncherApplicationServiceOptions);
|
|
10
|
-
handlers(): LauncherHandlers;
|
|
11
|
-
}
|
|
6
|
+
/** Bind coding-tool launch preparation to the live catalog's handler. */
|
|
7
|
+
export declare function launcherHandlers({ listModels }: LauncherHandlerDependencies): LauncherHandlers;
|
|
12
8
|
export {};
|