@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,91 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import { SubscriptionAccountSet } from "@velum-labs/routekit-accounts";
|
|
7
|
+
import { runRouteKitEffect } from "@velum-labs/routekit-runtime/effect";
|
|
8
|
+
import { codex, codexProvider } from "../module-api.js";
|
|
9
|
+
for (const stream of [false, true]) {
|
|
10
|
+
test(`managed Codex source preserves images and account attribution (stream=${stream})`, async (t) => {
|
|
11
|
+
const directory = await mkdtemp(join(tmpdir(), "codex-content-"));
|
|
12
|
+
await writeFile(join(directory, "account.json"), JSON.stringify({ tokens: { access_token: "offline-token", account_id: "offline-account" } }));
|
|
13
|
+
const accounts = await runRouteKitEffect(SubscriptionAccountSet.open(codexProvider(), {
|
|
14
|
+
source: { kind: "directory", path: directory },
|
|
15
|
+
publishedModels: ["codex-test"],
|
|
16
|
+
probeIntervalMs: 0
|
|
17
|
+
}));
|
|
18
|
+
t.after(async () => {
|
|
19
|
+
await runRouteKitEffect(accounts.close());
|
|
20
|
+
await rm(directory, { recursive: true, force: true });
|
|
21
|
+
});
|
|
22
|
+
const source = await runRouteKitEffect(codex.createSource({
|
|
23
|
+
sourceId: "codex",
|
|
24
|
+
config: { use: "codex" },
|
|
25
|
+
env: {},
|
|
26
|
+
resources: { accountSet: accounts }
|
|
27
|
+
}));
|
|
28
|
+
assert.equal(source.responses.kind, "unsupported");
|
|
29
|
+
const requests = [];
|
|
30
|
+
t.mock.method(globalThis, "fetch", async (input, init) => {
|
|
31
|
+
const request = new Request(input, init);
|
|
32
|
+
requests.push(request);
|
|
33
|
+
assert.equal(request.url, "https://chatgpt.com/backend-api/codex/responses");
|
|
34
|
+
const response = {
|
|
35
|
+
id: "resp_offline",
|
|
36
|
+
output: [
|
|
37
|
+
{
|
|
38
|
+
type: "message",
|
|
39
|
+
role: "assistant",
|
|
40
|
+
content: [{ type: "output_text", text: "image received" }]
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
usage: { input_tokens: 4, output_tokens: 3, total_tokens: 7 }
|
|
44
|
+
};
|
|
45
|
+
return new Response(`event: response.output_text.delta\ndata: {"delta":"image received"}\n\nevent: response.completed\ndata: ${JSON.stringify({ response })}\n\n`, { headers: { "content-type": "text/event-stream" } });
|
|
46
|
+
});
|
|
47
|
+
const attempts = [];
|
|
48
|
+
const response = await runRouteKitEffect(source.requests.chat({
|
|
49
|
+
model: "codex-test",
|
|
50
|
+
stream,
|
|
51
|
+
messages: [
|
|
52
|
+
{
|
|
53
|
+
role: "user",
|
|
54
|
+
content: [
|
|
55
|
+
{
|
|
56
|
+
type: "image_url",
|
|
57
|
+
image_url: { url: "data:image/png;base64,b2ZmbGluZQ==", detail: "low" }
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
max_tokens: 1
|
|
63
|
+
}, undefined, {
|
|
64
|
+
responseMode: stream ? "streaming" : "buffered",
|
|
65
|
+
onAttribution: (update) => attempts.push(update)
|
|
66
|
+
}));
|
|
67
|
+
assert.equal(response.status, 200);
|
|
68
|
+
const text = await response.text();
|
|
69
|
+
assert.match(text, /image received/);
|
|
70
|
+
assert.match(text, /prompt_tokens":4/);
|
|
71
|
+
assert.equal(requests.length, 1);
|
|
72
|
+
assert.equal(requests[0].headers.get("authorization"), "Bearer offline-token");
|
|
73
|
+
assert.equal(requests[0].headers.get("chatgpt-account-id"), "offline-account");
|
|
74
|
+
const body = (await requests[0].json());
|
|
75
|
+
assert.deepEqual(body.input.at(-1), {
|
|
76
|
+
role: "user",
|
|
77
|
+
content: [
|
|
78
|
+
{ type: "input_image", image_url: "data:image/png;base64,b2ZmbGluZQ==", detail: "low" }
|
|
79
|
+
]
|
|
80
|
+
});
|
|
81
|
+
assert.equal(body.stream, true);
|
|
82
|
+
assert.equal(Object.hasOwn(body, "max_output_tokens"), false);
|
|
83
|
+
assert.ok(attempts.some((attempt) => Object.hasOwn(attempt, "accountAttempt")));
|
|
84
|
+
const rejected = await runRouteKitEffect(source.requests.chat({
|
|
85
|
+
model: "codex-test",
|
|
86
|
+
messages: [{ role: "user", content: [{ type: "file", file: { file_data: "private" } }] }]
|
|
87
|
+
}));
|
|
88
|
+
assert.equal(rejected.status, 400);
|
|
89
|
+
assert.equal(requests.length, 1, "rejected content never enters the account transport");
|
|
90
|
+
});
|
|
91
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ApiProviderSource } from "@velum-labs/routekit-provider-core";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
export declare const custom: {
|
|
4
|
+
readonly id: "custom";
|
|
5
|
+
readonly protocol: "openai";
|
|
6
|
+
readonly accountClass: "api-key";
|
|
7
|
+
readonly billingMode: "metered-api";
|
|
8
|
+
readonly createSource: (context: import("@velum-labs/routekit-provider-core").ProviderModuleContext) => Effect.Effect<ApiProviderSource, never, never>;
|
|
9
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ApiProviderSource } from "@velum-labs/routekit-provider-core";
|
|
2
|
+
import { defineProvider } from "@velum-labs/routekit-provider-core";
|
|
3
|
+
import { Effect } from "effect";
|
|
4
|
+
export const custom = defineProvider({
|
|
5
|
+
id: "custom",
|
|
6
|
+
protocol: "openai",
|
|
7
|
+
accountClass: "api-key",
|
|
8
|
+
billingMode: "metered-api",
|
|
9
|
+
createSource: (context) => Effect.sync(() => new ApiProviderSource({
|
|
10
|
+
provider: "custom",
|
|
11
|
+
sourceId: context.sourceId,
|
|
12
|
+
config: context.config,
|
|
13
|
+
env: context.env
|
|
14
|
+
}))
|
|
15
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ApiProviderSource } from "@velum-labs/routekit-provider-core";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
export declare const google: {
|
|
4
|
+
readonly id: "google";
|
|
5
|
+
readonly protocol: "google";
|
|
6
|
+
readonly accountClass: "api-key";
|
|
7
|
+
readonly billingMode: "metered-api";
|
|
8
|
+
readonly connection: Readonly<{
|
|
9
|
+
validate?: (context: Readonly<{
|
|
10
|
+
sourceId: string;
|
|
11
|
+
config: import("@velum-labs/routekit-config-core").SourceConfig;
|
|
12
|
+
env: Readonly<Record<string, string | undefined>>;
|
|
13
|
+
resources?: Readonly<Record<string, unknown>>;
|
|
14
|
+
}>) => Effect.Effect<void, Error, import("@velum-labs/routekit-runtime/effect").RouteKitPlatform>;
|
|
15
|
+
credentialValidation?: "registry-key-probe" | "registry-key-probe-if-configured-models";
|
|
16
|
+
manifest: import("@velum-labs/routekit-provider-core").ProviderConnectionManifest;
|
|
17
|
+
prepare?: (input: import("@velum-labs/routekit-provider-core").PrepareProviderConnectionInput) => import("@velum-labs/routekit-provider-core").PreparedProviderConnection;
|
|
18
|
+
enrollment?: Readonly<Record<string, import("@velum-labs/routekit-provider-core").ProviderEnrollmentAdapter>>;
|
|
19
|
+
credentials?: import("@velum-labs/routekit-provider-core").ProviderCredentialAdapter;
|
|
20
|
+
subscriptionMode?: import("@velum-labs/routekit-registry").SubscriptionMode;
|
|
21
|
+
}>;
|
|
22
|
+
readonly createSource: (context: import("@velum-labs/routekit-provider-core").ProviderModuleContext) => Effect.Effect<ApiProviderSource, never, never>;
|
|
23
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ApiProviderSource, connectionString, connectionStringList, defineProviderConnection, defineProvider, optionalConnectionString, preparedProviderConnection, providerSourceTransportFromContext, routeTextToolModelMetadata } from "@velum-labs/routekit-provider-core";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
function configuredGoogleModelMetadata(model) {
|
|
4
|
+
const nativeModel = model.replace(/^models\//u, "");
|
|
5
|
+
return /^gemini(?:[-.]|$)/iu.test(nativeModel)
|
|
6
|
+
? routeTextToolModelMetadata({ inputModalities: ["text", "image"] })
|
|
7
|
+
: undefined;
|
|
8
|
+
}
|
|
9
|
+
export const google = defineProvider({
|
|
10
|
+
id: "google",
|
|
11
|
+
protocol: "google",
|
|
12
|
+
accountClass: "api-key",
|
|
13
|
+
billingMode: "metered-api",
|
|
14
|
+
connection: defineProviderConnection({
|
|
15
|
+
manifest: {
|
|
16
|
+
version: 1, id: "google", displayName: "Google Gemini API", category: "api",
|
|
17
|
+
description: "Connect a Google AI Studio API key.",
|
|
18
|
+
methods: [{ id: "api-key", kind: "form", label: "API key", schemaRevision: 1, fields: [
|
|
19
|
+
{ id: "api-key", label: "API key", control: "secret", required: true },
|
|
20
|
+
{ id: "base-url", label: "Base URL", control: "url", required: false, advanced: true, description: "With explicit models, custom endpoints must support authenticated GET /v1beta/models for key validation." },
|
|
21
|
+
{ id: "models", label: "Models", control: "string-list", required: false, advanced: true, description: "Optional comma- or line-separated IDs. Explicit models require read-only model-list access to validate the key." }
|
|
22
|
+
] }]
|
|
23
|
+
},
|
|
24
|
+
prepare: ({ values }) => {
|
|
25
|
+
const env = "ROUTEKIT_CLOUD_GRANT_GOOGLE_KEY";
|
|
26
|
+
const baseUrl = optionalConnectionString(values, "base-url");
|
|
27
|
+
const models = connectionStringList(values, "models");
|
|
28
|
+
return preparedProviderConnection({
|
|
29
|
+
config: { use: "google", auth: { kind: "env-header", env, header: "x-goog-api-key" }, ...(baseUrl === undefined ? {} : { baseUrl }), ...(models === undefined ? {} : { options: { models } }) },
|
|
30
|
+
environment: { [env]: connectionString(values, "api-key") }
|
|
31
|
+
});
|
|
32
|
+
},
|
|
33
|
+
credentialValidation: "registry-key-probe-if-configured-models"
|
|
34
|
+
}),
|
|
35
|
+
createSource: (context) => Effect.sync(() => new ApiProviderSource({
|
|
36
|
+
provider: "google",
|
|
37
|
+
sourceId: context.sourceId,
|
|
38
|
+
config: context.config,
|
|
39
|
+
env: context.env,
|
|
40
|
+
transport: providerSourceTransportFromContext(context),
|
|
41
|
+
metadataForConfiguredModel: configuredGoogleModelMetadata
|
|
42
|
+
}))
|
|
43
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { Effect } from "effect";
|
|
4
|
+
import { google } from "../index.js";
|
|
5
|
+
test("configured Google catalogs qualify known Gemini routes and fail closed otherwise", async () => {
|
|
6
|
+
const prepared = google.connection.prepare({
|
|
7
|
+
sourceId: "google-production",
|
|
8
|
+
methodId: "api-key",
|
|
9
|
+
values: { "api-key": "google-secret" }
|
|
10
|
+
});
|
|
11
|
+
const source = await Effect.runPromise(google.createSource({
|
|
12
|
+
sourceId: "google-production",
|
|
13
|
+
config: {
|
|
14
|
+
...prepared.config,
|
|
15
|
+
options: {
|
|
16
|
+
models: ["gemini-2.5-pro", "text-embedding-004", "future-google-model"]
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
env: prepared.environment
|
|
20
|
+
}));
|
|
21
|
+
const models = await Effect.runPromise(source.discovery.discoverModels());
|
|
22
|
+
const [gemini, embedding, unknown] = models;
|
|
23
|
+
assert.deepEqual(gemini?.metadata, {
|
|
24
|
+
architecture: {
|
|
25
|
+
modality: "text+image->text",
|
|
26
|
+
inputModalities: ["text", "image"],
|
|
27
|
+
outputModalities: ["text"]
|
|
28
|
+
},
|
|
29
|
+
supportedParameters: ["tools", "tool_choice"],
|
|
30
|
+
provenance: "route"
|
|
31
|
+
});
|
|
32
|
+
assert.equal(embedding?.metadata, undefined);
|
|
33
|
+
assert.equal(unknown?.metadata, undefined);
|
|
34
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { SourceConfig } from "@velum-labs/routekit-config-core";
|
|
2
|
+
import type { ProviderSource } from "@velum-labs/routekit-provider-core";
|
|
3
|
+
import { Effect } from "effect";
|
|
4
|
+
export type VertexAuthHeaders = (url: string) => Promise<{
|
|
5
|
+
entries(): IterableIterator<[string, string]>;
|
|
6
|
+
}>;
|
|
7
|
+
export declare function createVertexSource(sourceId: string, config: SourceConfig, authorization?: VertexAuthHeaders | Readonly<Record<string, string | undefined>>): ProviderSource;
|
|
8
|
+
export declare const googleVertex: {
|
|
9
|
+
readonly id: "google-vertex";
|
|
10
|
+
readonly protocol: "google";
|
|
11
|
+
readonly accountClass: "iam";
|
|
12
|
+
readonly billingMode: "metered-api";
|
|
13
|
+
readonly connection: Readonly<{
|
|
14
|
+
validate?: (context: Readonly<{
|
|
15
|
+
sourceId: string;
|
|
16
|
+
config: SourceConfig;
|
|
17
|
+
env: Readonly<Record<string, string | undefined>>;
|
|
18
|
+
resources?: Readonly<Record<string, unknown>>;
|
|
19
|
+
}>) => Effect.Effect<void, Error, import("@velum-labs/routekit-runtime/effect").RouteKitPlatform>;
|
|
20
|
+
credentialValidation?: "registry-key-probe" | "registry-key-probe-if-configured-models";
|
|
21
|
+
manifest: import("@velum-labs/routekit-provider-core").ProviderConnectionManifest;
|
|
22
|
+
prepare?: (input: import("@velum-labs/routekit-provider-core").PrepareProviderConnectionInput) => import("@velum-labs/routekit-provider-core").PreparedProviderConnection;
|
|
23
|
+
enrollment?: Readonly<Record<string, import("@velum-labs/routekit-provider-core").ProviderEnrollmentAdapter>>;
|
|
24
|
+
credentials?: import("@velum-labs/routekit-provider-core").ProviderCredentialAdapter;
|
|
25
|
+
subscriptionMode?: import("@velum-labs/routekit-registry").SubscriptionMode;
|
|
26
|
+
}>;
|
|
27
|
+
readonly createSource: (context: import("@velum-labs/routekit-provider-core").ProviderModuleContext) => Effect.Effect<ProviderSource, never, never>;
|
|
28
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { connectionString, connectionStringList, defineProviderConnection, GoogleGenAiBackend, optionalConnectionString, preparedProviderConnection, ProviderConnectionInputError, routeTextToolModelMetadata } from "@velum-labs/routekit-provider-core";
|
|
2
|
+
import { defineProvider } from "@velum-labs/routekit-provider-core";
|
|
3
|
+
import { executeWebRequest, RouteKitFailure, routeKitError } from "@velum-labs/routekit-runtime/effect";
|
|
4
|
+
import { Effect } from "effect";
|
|
5
|
+
import { GoogleAuth } from "google-auth-library";
|
|
6
|
+
function configuredVertexModelMetadata(model) {
|
|
7
|
+
return /(?:^|\/)gemini(?:[-.]|$)/iu.test(model)
|
|
8
|
+
? routeTextToolModelMetadata({ inputModalities: ["text", "image"] })
|
|
9
|
+
: undefined;
|
|
10
|
+
}
|
|
11
|
+
function option(config, key) {
|
|
12
|
+
const value = config.options?.[key];
|
|
13
|
+
if (typeof value !== "string" || value.length === 0)
|
|
14
|
+
throw new Error(`google-vertex requires options.${key}`);
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
function configuredModels(config) {
|
|
18
|
+
const value = config.options?.models;
|
|
19
|
+
if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string" && entry.length > 0)) {
|
|
20
|
+
throw new Error("google-vertex requires options.models");
|
|
21
|
+
}
|
|
22
|
+
return value.map((id) => {
|
|
23
|
+
const metadata = configuredVertexModelMetadata(id);
|
|
24
|
+
return { id, ...(metadata === undefined ? {} : { metadata }) };
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function adcHeaders(env) {
|
|
28
|
+
const serialized = env.GOOGLE_APPLICATION_CREDENTIALS_JSON;
|
|
29
|
+
const credentials = serialized === undefined
|
|
30
|
+
? undefined
|
|
31
|
+
: JSON.parse(serialized);
|
|
32
|
+
const auth = new GoogleAuth({
|
|
33
|
+
scopes: ["https://www.googleapis.com/auth/cloud-platform"],
|
|
34
|
+
...(credentials === undefined ? {} : { credentials })
|
|
35
|
+
});
|
|
36
|
+
return async (url) => {
|
|
37
|
+
const client = await auth.getClient();
|
|
38
|
+
return client.getRequestHeaders(url);
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function createVertexSource(sourceId, config, authorization = process.env) {
|
|
42
|
+
const getAuthHeaders = typeof authorization === "function"
|
|
43
|
+
? authorization
|
|
44
|
+
: adcHeaders(authorization);
|
|
45
|
+
if (config.auth?.kind !== "adc")
|
|
46
|
+
throw new Error("google-vertex requires auth.kind adc");
|
|
47
|
+
const project = option(config, "project");
|
|
48
|
+
const location = option(config, "location");
|
|
49
|
+
const publisher = typeof config.options?.publisher === "string" ? config.options.publisher : "google";
|
|
50
|
+
const transport = (url, init) => Effect.tryPromise({
|
|
51
|
+
try: async () => {
|
|
52
|
+
const headers = new Headers(init.headers);
|
|
53
|
+
const authHeaders = await getAuthHeaders(url);
|
|
54
|
+
for (const [name, value] of authHeaders.entries())
|
|
55
|
+
headers.set(name, value);
|
|
56
|
+
return { url, init: { ...init, headers } };
|
|
57
|
+
},
|
|
58
|
+
catch: (cause) => new RouteKitFailure({ message: "could not acquire Vertex ADC", cause })
|
|
59
|
+
}).pipe(Effect.flatMap(({ url, init }) => executeWebRequest(url, init)), Effect.mapError(routeKitError));
|
|
60
|
+
const backend = new GoogleGenAiBackend({
|
|
61
|
+
baseUrl: `https://${location}-aiplatform.googleapis.com/v1/projects/${encodeURIComponent(project)}/locations/${encodeURIComponent(location)}/publishers/${encodeURIComponent(publisher)}`,
|
|
62
|
+
apiKey: "",
|
|
63
|
+
apiKeyHeader: false,
|
|
64
|
+
transport,
|
|
65
|
+
modelPath: (model, method, streaming) => `/models/${encodeURIComponent(model)}:${method}${streaming ? "?alt=sse" : ""}`
|
|
66
|
+
});
|
|
67
|
+
return {
|
|
68
|
+
sourceId,
|
|
69
|
+
discovery: { discoverModels: () => Effect.succeed(configuredModels(config)) },
|
|
70
|
+
requests: {
|
|
71
|
+
chat: (body, signal, options) => backend.chat(body, signal, options),
|
|
72
|
+
embeddings: (_body, _signal, _options) => Effect.succeed(Response.json({ error: { message: "Vertex embeddings are not supported by this source" } }, { status: 501 }))
|
|
73
|
+
},
|
|
74
|
+
responses: { kind: "unsupported" },
|
|
75
|
+
capabilities: {
|
|
76
|
+
forModel: () => ({}),
|
|
77
|
+
metadataForModel: configuredVertexModelMetadata,
|
|
78
|
+
reasoningForModel: () => undefined
|
|
79
|
+
},
|
|
80
|
+
resource: { kind: "borrowed" }
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export const googleVertex = defineProvider({
|
|
84
|
+
id: "google-vertex",
|
|
85
|
+
protocol: "google",
|
|
86
|
+
accountClass: "iam",
|
|
87
|
+
billingMode: "metered-api",
|
|
88
|
+
connection: defineProviderConnection({
|
|
89
|
+
manifest: {
|
|
90
|
+
version: 1, id: "google-vertex", displayName: "Google Vertex AI", category: "cloud",
|
|
91
|
+
description: "Connect a Google Cloud service account to Vertex AI.",
|
|
92
|
+
methods: [{ id: "service-account", kind: "form", label: "Service account", schemaRevision: 1, fields: [
|
|
93
|
+
{ id: "service-account-json", label: "Service account JSON", control: "secret-json", required: true },
|
|
94
|
+
{ id: "project", label: "Google Cloud project", control: "text", required: true },
|
|
95
|
+
{ id: "location", label: "Location", control: "text", required: true, placeholder: "us-central1" },
|
|
96
|
+
{ id: "publisher", label: "Publisher", control: "text", required: false, advanced: true, defaultValue: "google" },
|
|
97
|
+
{ id: "models", label: "Models", control: "string-list", required: true, description: "Comma- or line-separated Vertex model IDs." }
|
|
98
|
+
] }]
|
|
99
|
+
},
|
|
100
|
+
prepare: ({ values }) => {
|
|
101
|
+
const serialized = connectionString(values, "service-account-json");
|
|
102
|
+
let credentials;
|
|
103
|
+
try {
|
|
104
|
+
credentials = JSON.parse(serialized);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
throw new ProviderConnectionInputError("The service account JSON is invalid.", {
|
|
108
|
+
"service-account-json": "Enter valid JSON."
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
if (typeof credentials !== "object" || credentials === null ||
|
|
112
|
+
typeof credentials.client_email !== "string" || credentials.client_email.length === 0 ||
|
|
113
|
+
typeof credentials.private_key !== "string" || credentials.private_key.length === 0) {
|
|
114
|
+
throw new ProviderConnectionInputError("The service account JSON is invalid.", {
|
|
115
|
+
"service-account-json": "The JSON must contain client_email and private_key."
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
const models = connectionStringList(values, "models");
|
|
119
|
+
if (models === undefined) {
|
|
120
|
+
throw new ProviderConnectionInputError("Vertex model IDs are required.", { models: "This field is required." });
|
|
121
|
+
}
|
|
122
|
+
return preparedProviderConnection({
|
|
123
|
+
config: {
|
|
124
|
+
use: "google-vertex", auth: { kind: "adc" },
|
|
125
|
+
options: {
|
|
126
|
+
project: connectionString(values, "project"),
|
|
127
|
+
location: connectionString(values, "location"),
|
|
128
|
+
publisher: optionalConnectionString(values, "publisher") ?? "google",
|
|
129
|
+
models
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
environment: { GOOGLE_APPLICATION_CREDENTIALS_JSON: JSON.stringify(credentials) }
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}),
|
|
136
|
+
createSource: (context) => Effect.sync(() => createVertexSource(context.sourceId, context.config, context.env))
|
|
137
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { runRouteKitEffect } from "@velum-labs/routekit-runtime/effect";
|
|
4
|
+
import { createVertexSource } from "../index.js";
|
|
5
|
+
test("Vertex sources use ADC and the regional publisher model path", async () => {
|
|
6
|
+
const originalFetch = globalThis.fetch;
|
|
7
|
+
let request;
|
|
8
|
+
let authUrl;
|
|
9
|
+
globalThis.fetch = async (input, init) => {
|
|
10
|
+
request = new Request(input, init);
|
|
11
|
+
return Response.json({
|
|
12
|
+
candidates: [{ content: { parts: [{ text: "vertex" }] } }],
|
|
13
|
+
usageMetadata: { promptTokenCount: 1, candidatesTokenCount: 1, totalTokenCount: 2 }
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
try {
|
|
17
|
+
const config = {
|
|
18
|
+
use: "google-vertex",
|
|
19
|
+
auth: { kind: "adc" },
|
|
20
|
+
options: {
|
|
21
|
+
project: "routekit-project",
|
|
22
|
+
location: "us-central1",
|
|
23
|
+
publisher: "google",
|
|
24
|
+
models: ["gemini-2.5-pro"]
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
const source = createVertexSource("vertex-prod", config, async (url) => {
|
|
28
|
+
authUrl = url;
|
|
29
|
+
return new Headers({ authorization: "Bearer adc-token" });
|
|
30
|
+
});
|
|
31
|
+
const response = await runRouteKitEffect(source.requests.chat({
|
|
32
|
+
model: "gemini-2.5-pro",
|
|
33
|
+
messages: [{ role: "user", content: "hello" }]
|
|
34
|
+
}));
|
|
35
|
+
const expectedUrl = "https://us-central1-aiplatform.googleapis.com/v1/projects/routekit-project/locations/us-central1/publishers/google/models/gemini-2.5-pro:generateContent";
|
|
36
|
+
assert.equal(source.sourceId, "vertex-prod");
|
|
37
|
+
assert.equal(response.status, 200);
|
|
38
|
+
assert.equal(authUrl, expectedUrl);
|
|
39
|
+
assert.equal(request?.url, expectedUrl);
|
|
40
|
+
assert.equal(request?.headers.get("authorization"), "Bearer adc-token");
|
|
41
|
+
assert.equal(request?.headers.get("x-goog-api-key"), null);
|
|
42
|
+
assert.deepEqual(await runRouteKitEffect(source.discovery.discoverModels()), [
|
|
43
|
+
{
|
|
44
|
+
id: "gemini-2.5-pro",
|
|
45
|
+
metadata: {
|
|
46
|
+
architecture: {
|
|
47
|
+
modality: "text+image->text",
|
|
48
|
+
inputModalities: ["text", "image"],
|
|
49
|
+
outputModalities: ["text"]
|
|
50
|
+
},
|
|
51
|
+
supportedParameters: ["tools", "tool_choice"],
|
|
52
|
+
provenance: "route"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
]);
|
|
56
|
+
const body = (await response.json());
|
|
57
|
+
assert.equal(body.choices[0]?.message.content, "vertex");
|
|
58
|
+
const embeddings = await runRouteKitEffect(source.requests.embeddings({ model: "text-embedding-005", input: ["hello"] }));
|
|
59
|
+
assert.equal(embeddings.status, 501);
|
|
60
|
+
assert.deepEqual(await embeddings.json(), {
|
|
61
|
+
error: { message: "Vertex embeddings are not supported by this source" }
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
globalThis.fetch = originalFetch;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
test("Vertex configured catalogs qualify Gemini and fail closed for embedding or unknown IDs", async () => {
|
|
69
|
+
const source = createVertexSource("vertex-prod", {
|
|
70
|
+
use: "google-vertex",
|
|
71
|
+
auth: { kind: "adc" },
|
|
72
|
+
options: {
|
|
73
|
+
project: "routekit-project",
|
|
74
|
+
location: "us-central1",
|
|
75
|
+
publisher: "google",
|
|
76
|
+
models: ["gemini-2.5-pro", "text-embedding-005", "future-vertex-model"]
|
|
77
|
+
}
|
|
78
|
+
}, async () => new Headers({ authorization: "Bearer adc-token" }));
|
|
79
|
+
const models = await runRouteKitEffect(source.discovery.discoverModels());
|
|
80
|
+
assert.notEqual(models[0]?.metadata, undefined);
|
|
81
|
+
assert.equal(models[1]?.metadata, undefined);
|
|
82
|
+
assert.equal(models[2]?.metadata, undefined);
|
|
83
|
+
assert.equal(source.capabilities.metadataForModel?.("text-embedding-005"), undefined);
|
|
84
|
+
assert.equal(source.capabilities.metadataForModel?.("future-vertex-model"), undefined);
|
|
85
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exact model-specific capabilities from OpenAI's published model pages.
|
|
3
|
+
* The global Responses schema's effort enum is not evidence for any model.
|
|
4
|
+
* Keep this list narrow; unknown and custom model IDs remain unconstrained.
|
|
5
|
+
*
|
|
6
|
+
* https://developers.openai.com/api/docs/models/gpt-6-luna
|
|
7
|
+
* https://developers.openai.com/api/docs/models/gpt-6-sol
|
|
8
|
+
* https://developers.openai.com/api/docs/models/gpt-6-astra
|
|
9
|
+
*/
|
|
10
|
+
const documentedEfforts = new Map([
|
|
11
|
+
["gpt-6-luna", ["none", "low", "medium", "high", "xhigh", "max"]],
|
|
12
|
+
["gpt-6-sol", ["none", "low", "medium", "high", "xhigh", "max"]],
|
|
13
|
+
["gpt-6-astra", ["low", "medium", "high", "xhigh", "max"]]
|
|
14
|
+
]);
|
|
15
|
+
export function documentedOpenAiReasoning(model) {
|
|
16
|
+
const efforts = documentedEfforts.get(model);
|
|
17
|
+
if (efforts === undefined)
|
|
18
|
+
return undefined;
|
|
19
|
+
return {
|
|
20
|
+
status: "supported",
|
|
21
|
+
efforts: efforts.map((id) => ({ id })),
|
|
22
|
+
defaultEffort: "medium",
|
|
23
|
+
wireShape: "openai-responses",
|
|
24
|
+
provenance: "builtin"
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ApiProviderSource, type ProviderModuleContext } from "@velum-labs/routekit-provider-core";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
export declare const openai: {
|
|
4
|
+
readonly id: "openai";
|
|
5
|
+
readonly protocol: "openai";
|
|
6
|
+
readonly accountClass: "api-key";
|
|
7
|
+
readonly billingMode: "metered-api";
|
|
8
|
+
readonly connection: Readonly<{
|
|
9
|
+
validate?: (context: Readonly<{
|
|
10
|
+
sourceId: string;
|
|
11
|
+
config: import("@velum-labs/routekit-config-core").SourceConfig;
|
|
12
|
+
env: Readonly<Record<string, string | undefined>>;
|
|
13
|
+
resources?: Readonly<Record<string, unknown>>;
|
|
14
|
+
}>) => Effect.Effect<void, Error, import("@velum-labs/routekit-runtime/effect").RouteKitPlatform>;
|
|
15
|
+
credentialValidation?: "registry-key-probe" | "registry-key-probe-if-configured-models";
|
|
16
|
+
manifest: import("@velum-labs/routekit-provider-core").ProviderConnectionManifest;
|
|
17
|
+
prepare?: (input: import("@velum-labs/routekit-provider-core").PrepareProviderConnectionInput) => import("@velum-labs/routekit-provider-core").PreparedProviderConnection;
|
|
18
|
+
enrollment?: Readonly<Record<string, import("@velum-labs/routekit-provider-core").ProviderEnrollmentAdapter>>;
|
|
19
|
+
credentials?: import("@velum-labs/routekit-provider-core").ProviderCredentialAdapter;
|
|
20
|
+
subscriptionMode?: import("@velum-labs/routekit-registry").SubscriptionMode;
|
|
21
|
+
}>;
|
|
22
|
+
readonly createSource: (context: ProviderModuleContext) => Effect.Effect<ApiProviderSource, never, never>;
|
|
23
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { ApiProviderSource, connectionString, connectionStringList, defineProviderConnection, defineProvider, OpenAiApiSpecificationClient, optionalProviderModuleResource, optionalConnectionString, preparedProviderConnection, providerSourceTransportFromContext, sharedOpenAiApiSpecificationClient } from "@velum-labs/routekit-provider-core";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import { documentedOpenAiReasoning } from "./documented-reasoning.js";
|
|
4
|
+
function usesOfficialOpenAiApi(context) {
|
|
5
|
+
const baseUrl = context.config.baseUrl ?? context.env.OPENAI_BASE_URL;
|
|
6
|
+
if (baseUrl === undefined)
|
|
7
|
+
return true;
|
|
8
|
+
try {
|
|
9
|
+
const url = new URL(baseUrl);
|
|
10
|
+
return url.origin === "https://api.openai.com";
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function apiSpecificationClient(context) {
|
|
17
|
+
const injected = optionalProviderModuleResource(context, "openaiApiSpecification", (value) => value instanceof OpenAiApiSpecificationClient);
|
|
18
|
+
return injected ?? sharedOpenAiApiSpecificationClient();
|
|
19
|
+
}
|
|
20
|
+
export const openai = defineProvider({
|
|
21
|
+
id: "openai",
|
|
22
|
+
protocol: "openai",
|
|
23
|
+
accountClass: "api-key",
|
|
24
|
+
billingMode: "metered-api",
|
|
25
|
+
connection: defineProviderConnection({
|
|
26
|
+
manifest: {
|
|
27
|
+
version: 1,
|
|
28
|
+
id: "openai",
|
|
29
|
+
displayName: "OpenAI API",
|
|
30
|
+
category: "api",
|
|
31
|
+
description: "Connect an OpenAI project API key.",
|
|
32
|
+
methods: [{
|
|
33
|
+
id: "api-key", kind: "form", label: "API key", schemaRevision: 1,
|
|
34
|
+
fields: [
|
|
35
|
+
{ id: "api-key", label: "API key", control: "secret", required: true, placeholder: "sk-…" },
|
|
36
|
+
{ id: "base-url", label: "Base URL", control: "url", required: false, advanced: true, placeholder: "https://api.openai.com", description: "With explicit models, custom endpoints must support authenticated GET /v1/models for key validation." },
|
|
37
|
+
{ id: "models", label: "Models", control: "string-list", required: false, advanced: true, description: "Optional comma- or line-separated IDs. Explicit models require read-only model-list access to validate the key." }
|
|
38
|
+
]
|
|
39
|
+
}]
|
|
40
|
+
},
|
|
41
|
+
prepare: ({ values }) => {
|
|
42
|
+
const env = "ROUTEKIT_CLOUD_GRANT_OPENAI_KEY";
|
|
43
|
+
const baseUrl = optionalConnectionString(values, "base-url");
|
|
44
|
+
const models = connectionStringList(values, "models");
|
|
45
|
+
return preparedProviderConnection({
|
|
46
|
+
config: { use: "openai", auth: { kind: "env-bearer", env }, ...(baseUrl === undefined ? {} : { baseUrl }), ...(models === undefined ? {} : { options: { models } }) },
|
|
47
|
+
environment: { [env]: connectionString(values, "api-key") }
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
credentialValidation: "registry-key-probe-if-configured-models"
|
|
51
|
+
}),
|
|
52
|
+
createSource: (context) => Effect.sync(() => {
|
|
53
|
+
const official = usesOfficialOpenAiApi(context);
|
|
54
|
+
let enrichDiscoveredModels;
|
|
55
|
+
if (official) {
|
|
56
|
+
const specification = apiSpecificationClient(context);
|
|
57
|
+
enrichDiscoveredModels = (models, signal) => specification.enrich(models, signal).pipe(Effect.catch((error) => Effect.logWarning("OpenAI API specification is unavailable; retaining sparse model discovery").pipe(Effect.annotateLogs({
|
|
58
|
+
sourceId: context.sourceId,
|
|
59
|
+
stage: "openai_api_specification",
|
|
60
|
+
error: error.message
|
|
61
|
+
}), Effect.as(models))));
|
|
62
|
+
}
|
|
63
|
+
return new ApiProviderSource({
|
|
64
|
+
provider: "openai",
|
|
65
|
+
sourceId: context.sourceId,
|
|
66
|
+
config: context.config,
|
|
67
|
+
env: context.env,
|
|
68
|
+
transport: providerSourceTransportFromContext(context),
|
|
69
|
+
nativeResponses: true,
|
|
70
|
+
...(official ? { reasoningForModel: documentedOpenAiReasoning } : {}),
|
|
71
|
+
enrichDiscoveredModels
|
|
72
|
+
});
|
|
73
|
+
})
|
|
74
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|