mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-17 12:22:34 +03:00
fix(opencode): read management token from environment with startup fallback warning (#13613)
The opencode plugin v2 reads its management token from `OMNIROUTE_MANAGEMENT_API_KEY` (the plugin option still wins) and warns once at startup when it has to fall back to the inference key. Eight cases through the real plugin setup, env isolated, asserting the Bearer header on `/api/*`.
Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.
Thanks @maxmad64bis!
This commit is contained in:
@@ -56,8 +56,14 @@ explicitly:
|
||||
}
|
||||
```
|
||||
|
||||
The token can also come from the `OMNIROUTE_MANAGEMENT_API_KEY` environment
|
||||
variable (the option wins when both are set). Resolution order:
|
||||
`managementReadToken` option, then `OMNIROUTE_MANAGEMENT_API_KEY`, then the
|
||||
`apiKey` fallback.
|
||||
|
||||
Left unset, `managementReadToken` falls back to `apiKey` for backwards
|
||||
compatibility. When a gateway rejects that fallback, the catalog still
|
||||
compatibility, and the plugin warns once at startup that the fallback is
|
||||
active. When a gateway rejects that fallback, the catalog still
|
||||
publishes — but with raw model ids instead of display names, no canonical
|
||||
alias dedupe, no pricing and no combos. The plugin warns once per endpoint
|
||||
when this happens, naming the endpoint and the consequence, so the degraded
|
||||
@@ -65,25 +71,25 @@ catalog is never a mystery.
|
||||
|
||||
## Options
|
||||
|
||||
| Key | Default | Notes |
|
||||
| -------------------------------- | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
|
||||
| `providerId` | `"omniroute"` | Provider id and integration id; models publish under `<providerId>/…` |
|
||||
| `baseURL` | required | OmniRoute gateway root (no `/v1` suffix needed) |
|
||||
| `apiKey` | connected credential, then `OMNIROUTE_API_KEY` | Chat key for `/v1/*` — see [Credentials](#credentials) |
|
||||
| `managementReadToken` | falls back to `apiKey` | Management key for `/api/*` (combos, providers, enrichment) — usually **not** the same key |
|
||||
| `displayName` | `"OmniRoute"` | Provider display name |
|
||||
| `timeoutMs` | `10000` | Per-endpoint fetch timeout (auto-combos use 5s) |
|
||||
| `modelCacheTtlMs` | `300000` | Catalog cache TTL; disk snapshot warms cold starts |
|
||||
| `timeouts` | per-endpoint override | `{ models, combos, autoCombos, enrichment }` in ms; falls back to `timeoutMs` |
|
||||
| `enrichment` | `true` | Fetch names + pricing (`/api/pricing*`, `/api/free-tier/summary`) |
|
||||
| `providerTag` | `true` | Prefix a display name with the upstream provider it routes to |
|
||||
| `geminiSanitization` | `true` | Strip `$schema`/`additionalProperties` from tool schemas sent to Gemini models (`$ref` tools are forwarded untouched) |
|
||||
| `usableOnly` | `false` | Filter to healthy provisioned providers (`/api/providers`) |
|
||||
| `visibleModels` / `hiddenModels` | `[]` | Exact-or-suffix allowlists, deny wins |
|
||||
| `apiFormat.allowAnthropic` | `false` | Route allowlisted ids to the Anthropic API block |
|
||||
| `apiFormat.anthropicModels` | `[]` | Full model ids routed to Anthropic |
|
||||
| `apiFormat.anthropicPrefixes` | v1 defaults | Deprecated, warns once — prefer `anthropicModels` |
|
||||
| `logLevel` / `startupDebug` | `warn` / `false` | Logger verbosity |
|
||||
| Key | Default | Notes |
|
||||
| -------------------------------- | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
|
||||
| `providerId` | `"omniroute"` | Provider id and integration id; models publish under `<providerId>/…` |
|
||||
| `baseURL` | required | OmniRoute gateway root (no `/v1` suffix needed) |
|
||||
| `apiKey` | connected credential, then `OMNIROUTE_API_KEY` | Chat key for `/v1/*` — see [Credentials](#credentials) |
|
||||
| `managementReadToken` | option, then `OMNIROUTE_MANAGEMENT_API_KEY`, then `apiKey` | Management key for `/api/*` (combos, providers, enrichment) — usually **not** the same key |
|
||||
| `displayName` | `"OmniRoute"` | Provider display name |
|
||||
| `timeoutMs` | `10000` | Per-endpoint fetch timeout (auto-combos use 5s) |
|
||||
| `modelCacheTtlMs` | `300000` | Catalog cache TTL; disk snapshot warms cold starts |
|
||||
| `timeouts` | per-endpoint override | `{ models, combos, autoCombos, enrichment }` in ms; falls back to `timeoutMs` |
|
||||
| `enrichment` | `true` | Fetch names + pricing (`/api/pricing*`, `/api/free-tier/summary`) |
|
||||
| `providerTag` | `true` | Prefix a display name with the upstream provider it routes to |
|
||||
| `geminiSanitization` | `true` | Strip `$schema`/`additionalProperties` from tool schemas sent to Gemini models (`$ref` tools are forwarded untouched) |
|
||||
| `usableOnly` | `false` | Filter to healthy provisioned providers (`/api/providers`) |
|
||||
| `visibleModels` / `hiddenModels` | `[]` | Exact-or-suffix allowlists, deny wins |
|
||||
| `apiFormat.allowAnthropic` | `false` | Route allowlisted ids to the Anthropic API block |
|
||||
| `apiFormat.anthropicModels` | `[]` | Full model ids routed to Anthropic |
|
||||
| `apiFormat.anthropicPrefixes` | v1 defaults | Deprecated, warns once — prefer `anthropicModels` |
|
||||
| `logLevel` / `startupDebug` | `warn` / `false` | Logger verbosity |
|
||||
|
||||
## Tool calling on Gemini models
|
||||
|
||||
|
||||
@@ -31,7 +31,14 @@ import { assertContext } from "./compat.js";
|
||||
import { type ApiKeyOrigin, resolveApiKey, warnIfMissing } from "./credentials.js";
|
||||
import { createSourceErrorReporter } from "./enrichment-report.js";
|
||||
import { sanitizeToolSchemasFor } from "./gemini-language.js";
|
||||
import { PLUGIN_ID, parsePluginOptions, resolveTimeouts, type PluginOptions } from "./options.js";
|
||||
import {
|
||||
MANAGEMENT_TOKEN_ENV_VAR,
|
||||
PLUGIN_ID,
|
||||
parsePluginOptions,
|
||||
resolveManagementReadToken,
|
||||
resolveTimeouts,
|
||||
type PluginOptions,
|
||||
} from "./options.js";
|
||||
|
||||
/**
|
||||
* A fetch result that says whether it succeeded. Returning a bare `[]` on
|
||||
@@ -61,7 +68,7 @@ function toResolvedOptions(parsed: PluginOptions): ResolvedOptions {
|
||||
providerId: parsed.providerId,
|
||||
baseURL: parsed.baseURL,
|
||||
apiKey: parsed.apiKey ?? process.env.OMNIROUTE_API_KEY ?? "",
|
||||
managementReadToken: parsed.managementReadToken,
|
||||
managementReadToken: resolveManagementReadToken(parsed.managementReadToken),
|
||||
timeoutMs: parsed.timeoutMs,
|
||||
timeouts: parsed.timeouts,
|
||||
logLevel: parsed.logLevel,
|
||||
@@ -93,6 +100,16 @@ export default define({
|
||||
resolved.logLevel = parsed.logLevel;
|
||||
resolved.startupDebug = parsed.startupDebug;
|
||||
log.info(`[omniroute-v2] init providerId=${X}`);
|
||||
// The inference key stands in below when no management token is set, and
|
||||
// gateways usually reject that stand-in with 401/403. Say so once here,
|
||||
// before any fetch, instead of letting the refusal surface per endpoint.
|
||||
if (resolved.managementReadToken === undefined) {
|
||||
log.warn(
|
||||
`[omniroute-v2] no management token configured: management endpoints (/api/*) will reuse the inference key, ` +
|
||||
`which gateways usually reject with 401/403. Set "managementReadToken" in the plugin options ` +
|
||||
`or export ${MANAGEMENT_TOKEN_ENV_VAR}.`
|
||||
);
|
||||
}
|
||||
|
||||
// v1 parity port: in-memory TTL + disk snapshot. The memory key
|
||||
// `baseURL::sha256(creds)` isolates credential tuples (prod vs
|
||||
|
||||
@@ -61,6 +61,21 @@ const pluginOptionsSchema = z
|
||||
|
||||
export type PluginOptions = z.infer<typeof pluginOptionsSchema>;
|
||||
|
||||
/** Environment source for the management token (option wins over this). */
|
||||
export const MANAGEMENT_TOKEN_ENV_VAR = "OMNIROUTE_MANAGEMENT_API_KEY";
|
||||
|
||||
/**
|
||||
* Resolve the management token: a non-empty option wins, then a non-empty
|
||||
* environment value, else absent. Empty counts as absent on both inputs, the
|
||||
* same rule the inference key follows; no trimming, the token is opaque.
|
||||
*/
|
||||
export function resolveManagementReadToken(optionValue: string | undefined): string | undefined {
|
||||
if (optionValue !== undefined && optionValue.length > 0) return optionValue;
|
||||
const fromEnv = process.env[MANAGEMENT_TOKEN_ENV_VAR];
|
||||
if (fromEnv !== undefined && fromEnv.length > 0) return fromEnv;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Per-endpoint timeout defaults (v1 parity). `timeoutMs` is the global fallback. */
|
||||
export const DEFAULT_TIMEOUT_MS = 10_000 as const;
|
||||
/** Auto-combos keep the v1 5s budget; the field is resolved now for the P3 port. */
|
||||
|
||||
371
@omniroute/opencode-plugin-v2/tests/management-token-env.test.ts
Normal file
371
@omniroute/opencode-plugin-v2/tests/management-token-env.test.ts
Normal file
@@ -0,0 +1,371 @@
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtempSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import plugin from "../src/index.js";
|
||||
import { publishCatalog } from "../src/catalog.js";
|
||||
import type { CatalogDraft } from "@opencode-ai/plugin/v2/promise";
|
||||
import type { ModelV2Info, ProviderV2Info } from "@opencode-ai/sdk/v2/types";
|
||||
|
||||
const MODELS_URL = "https://gw.example.com/v1/models";
|
||||
const COMBOS_URL = "https://gw.example.com/api/combos";
|
||||
const PRICING_MODELS_URL = "https://gw.example.com/api/pricing/models";
|
||||
|
||||
const MGMT_ENV_VAR = "OMNIROUTE_MANAGEMENT_API_KEY";
|
||||
const INFERENCE_ENV_VAR = "OMNIROUTE_API_KEY";
|
||||
|
||||
function okJson(body: unknown) {
|
||||
return { ok: true, status: 200, statusText: "OK", json: async () => body };
|
||||
}
|
||||
|
||||
interface Harness {
|
||||
seen: Map<string, string>;
|
||||
warns: string[];
|
||||
restore: () => void;
|
||||
}
|
||||
|
||||
function installHarness(combos: unknown[]): Harness {
|
||||
const seen = new Map<string, string>();
|
||||
const warns: string[] = [];
|
||||
const origFetch = globalThis.fetch;
|
||||
const origWarn = console.warn;
|
||||
const origLog = console.log;
|
||||
const origError = console.error;
|
||||
console.warn = (...args: unknown[]) => {
|
||||
warns.push(String(args[0]));
|
||||
};
|
||||
console.log = () => {};
|
||||
console.error = (...args: unknown[]) => {
|
||||
warns.push(String(args[0]));
|
||||
};
|
||||
globalThis.fetch = (async (url: unknown, init?: { headers?: Record<string, string> }) => {
|
||||
const href = String(url);
|
||||
seen.set(href, String(init?.headers?.Authorization ?? ""));
|
||||
if (href.includes("/api/combos/auto")) return okJson({ combos: [] });
|
||||
if (href.includes("/api/pricing/models")) {
|
||||
return okJson({
|
||||
providers: {
|
||||
demo: {
|
||||
id: "demo",
|
||||
name: "Demo",
|
||||
models: [{ id: "team-combo", name: "Team Combo" }],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
if (href.includes("/api/pricing")) return okJson({});
|
||||
if (href.includes("/api/free-tier/summary")) return okJson({ perModel: [] });
|
||||
if (href.includes("/api/combos")) return okJson({ combos });
|
||||
return okJson({ data: [{ id: "m1" }] });
|
||||
}) as typeof fetch;
|
||||
return {
|
||||
seen,
|
||||
warns,
|
||||
restore() {
|
||||
globalThis.fetch = origFetch;
|
||||
console.warn = origWarn;
|
||||
console.log = origLog;
|
||||
console.error = origError;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function withIsolatedEnv<T>(
|
||||
mgmt: string | undefined,
|
||||
inference: string | undefined,
|
||||
fn: () => Promise<T>
|
||||
): Promise<T> {
|
||||
const prevMgmt = process.env[MGMT_ENV_VAR];
|
||||
const prevInference = process.env[INFERENCE_ENV_VAR];
|
||||
// Like tests/management-token.test.ts:176-180: a fresh OPENCODE_DATA_DIR
|
||||
// per case keeps the real disk snapshot out of the run, so a filtered 'it'
|
||||
// never gets a warm snapshot served without fetch.
|
||||
const prevDataDir = process.env.OPENCODE_DATA_DIR;
|
||||
process.env.OPENCODE_DATA_DIR = mkdtempSync(join(tmpdir(), "omniroute-mgmt-env-"));
|
||||
if (mgmt === undefined) delete process.env[MGMT_ENV_VAR];
|
||||
else process.env[MGMT_ENV_VAR] = mgmt;
|
||||
if (inference === undefined) delete process.env[INFERENCE_ENV_VAR];
|
||||
else process.env[INFERENCE_ENV_VAR] = inference;
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
if (prevDataDir === undefined) delete process.env.OPENCODE_DATA_DIR;
|
||||
else process.env.OPENCODE_DATA_DIR = prevDataDir;
|
||||
if (prevMgmt === undefined) delete process.env[MGMT_ENV_VAR];
|
||||
else process.env[MGMT_ENV_VAR] = prevMgmt;
|
||||
if (prevInference === undefined) delete process.env[INFERENCE_ENV_VAR];
|
||||
else process.env[INFERENCE_ENV_VAR] = prevInference;
|
||||
}
|
||||
}
|
||||
|
||||
function setupHarness(options: Record<string, unknown>) {
|
||||
const catalogCallbacks: Array<(draft: unknown) => Promise<void>> = [];
|
||||
const ctx = {
|
||||
options,
|
||||
catalog: {
|
||||
transform: (cb: (draft: unknown) => Promise<void>) => {
|
||||
catalogCallbacks.push(cb);
|
||||
return Promise.resolve({ dispose: async () => {} });
|
||||
},
|
||||
},
|
||||
integration: {
|
||||
transform: () => Promise.resolve({ dispose: async () => {} }),
|
||||
},
|
||||
};
|
||||
return { catalogCallbacks, ctx };
|
||||
}
|
||||
|
||||
function stubDraft() {
|
||||
const published = new Map<string, Record<string, unknown>>();
|
||||
const draft = {
|
||||
provider: { update: (_id: string, fn: (p: Record<string, unknown>) => void) => fn({}) },
|
||||
model: {
|
||||
update: (pid: string, mid: string, fn: (m: Record<string, unknown>) => void) => {
|
||||
const key = pid + "/" + mid;
|
||||
let entry = published.get(key);
|
||||
if (entry === undefined) {
|
||||
entry = { id: mid, providerID: pid };
|
||||
published.set(key, entry);
|
||||
}
|
||||
fn(entry);
|
||||
},
|
||||
},
|
||||
};
|
||||
return { draft, published };
|
||||
}
|
||||
|
||||
function fallbackWarns(warns: string[]): string[] {
|
||||
return warns.filter((w) => w.includes("managementReadToken"));
|
||||
}
|
||||
|
||||
async function runSetup(ctx: unknown): Promise<void> {
|
||||
await (plugin as unknown as { setup: (ctx: unknown) => Promise<void> }).setup(ctx);
|
||||
}
|
||||
|
||||
describe("plugin-v2 management token environment source", () => {
|
||||
it("uses the managementReadToken option for /api/* while models keep apiKey", async () => {
|
||||
await withIsolatedEnv(undefined, undefined, async () => {
|
||||
const h = installHarness([]);
|
||||
try {
|
||||
const { catalogCallbacks, ctx } = setupHarness({
|
||||
baseURL: "https://gw.example.com",
|
||||
providerId: "omniroute",
|
||||
apiKey: "chat-key",
|
||||
managementReadToken: "mgmt-option-token",
|
||||
});
|
||||
await runSetup(ctx);
|
||||
assert.deepEqual(fallbackWarns(h.warns), []);
|
||||
const { draft } = stubDraft();
|
||||
await catalogCallbacks[0](draft);
|
||||
assert.equal(h.seen.get(COMBOS_URL), "Bearer mgmt-option-token");
|
||||
assert.equal(h.seen.get(MODELS_URL), "Bearer chat-key");
|
||||
} finally {
|
||||
h.restore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("reads the management token from the environment when the option is absent", async () => {
|
||||
await withIsolatedEnv("mgmt-env-token", undefined, async () => {
|
||||
const h = installHarness([]);
|
||||
try {
|
||||
const { catalogCallbacks, ctx } = setupHarness({
|
||||
baseURL: "https://gw.example.com",
|
||||
providerId: "omniroute",
|
||||
apiKey: "chat-key",
|
||||
});
|
||||
await runSetup(ctx);
|
||||
assert.deepEqual(fallbackWarns(h.warns), []);
|
||||
const { draft } = stubDraft();
|
||||
await catalogCallbacks[0](draft);
|
||||
assert.equal(h.seen.get(COMBOS_URL), "Bearer mgmt-env-token");
|
||||
assert.equal(h.seen.get(MODELS_URL), "Bearer chat-key");
|
||||
} finally {
|
||||
h.restore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("prefers the option over the environment", async () => {
|
||||
await withIsolatedEnv("mgmt-env-token", undefined, async () => {
|
||||
const h = installHarness([]);
|
||||
try {
|
||||
const { catalogCallbacks, ctx } = setupHarness({
|
||||
baseURL: "https://gw.example.com",
|
||||
providerId: "omniroute",
|
||||
apiKey: "chat-key",
|
||||
managementReadToken: "mgmt-option-token",
|
||||
});
|
||||
await runSetup(ctx);
|
||||
assert.deepEqual(fallbackWarns(h.warns), []);
|
||||
const { draft } = stubDraft();
|
||||
await catalogCallbacks[0](draft);
|
||||
assert.equal(h.seen.get(COMBOS_URL), "Bearer mgmt-option-token");
|
||||
} finally {
|
||||
h.restore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to the inference key with a single early warning when neither is set", async () => {
|
||||
await withIsolatedEnv(undefined, undefined, async () => {
|
||||
const h = installHarness([]);
|
||||
try {
|
||||
const { catalogCallbacks, ctx } = setupHarness({
|
||||
baseURL: "https://gw.example.com",
|
||||
providerId: "omniroute",
|
||||
apiKey: "chat-key",
|
||||
});
|
||||
await runSetup(ctx);
|
||||
const atSetup = fallbackWarns(h.warns);
|
||||
assert.equal(
|
||||
atSetup.length,
|
||||
1,
|
||||
`expected exactly one early fallback warning, got: ${JSON.stringify(h.warns)}`
|
||||
);
|
||||
assert.match(atSetup[0] ?? "", /managementReadToken/);
|
||||
assert.match(atSetup[0] ?? "", new RegExp(MGMT_ENV_VAR));
|
||||
assert.ok(!(atSetup[0] ?? "").includes("chat-key"), "warning must not leak the key");
|
||||
const { draft } = stubDraft();
|
||||
await catalogCallbacks[0](draft);
|
||||
assert.equal(h.seen.get(COMBOS_URL), "Bearer chat-key");
|
||||
assert.equal(
|
||||
fallbackWarns(h.warns).length,
|
||||
1,
|
||||
"the fallback warning stays a single setup-time notice"
|
||||
);
|
||||
} finally {
|
||||
h.restore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("treats an empty option as absent so the environment wins", async () => {
|
||||
await withIsolatedEnv("mgmt-env-token", undefined, async () => {
|
||||
const h = installHarness([]);
|
||||
try {
|
||||
const { catalogCallbacks, ctx } = setupHarness({
|
||||
baseURL: "https://gw.example.com",
|
||||
providerId: "omniroute",
|
||||
apiKey: "chat-key",
|
||||
managementReadToken: "",
|
||||
});
|
||||
await runSetup(ctx);
|
||||
assert.deepEqual(fallbackWarns(h.warns), []);
|
||||
const { draft } = stubDraft();
|
||||
await catalogCallbacks[0](draft);
|
||||
assert.equal(h.seen.get(COMBOS_URL), "Bearer mgmt-env-token");
|
||||
} finally {
|
||||
h.restore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("treats an empty environment value as absent so the option wins", async () => {
|
||||
await withIsolatedEnv("", undefined, async () => {
|
||||
const h = installHarness([]);
|
||||
try {
|
||||
const { catalogCallbacks, ctx } = setupHarness({
|
||||
baseURL: "https://gw.example.com",
|
||||
providerId: "omniroute",
|
||||
apiKey: "chat-key",
|
||||
managementReadToken: "mgmt-option-token",
|
||||
});
|
||||
await runSetup(ctx);
|
||||
assert.deepEqual(fallbackWarns(h.warns), []);
|
||||
const { draft } = stubDraft();
|
||||
await catalogCallbacks[0](draft);
|
||||
assert.equal(h.seen.get(COMBOS_URL), "Bearer mgmt-option-token");
|
||||
} finally {
|
||||
h.restore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back with a warning when both the option and the environment are empty", async () => {
|
||||
await withIsolatedEnv("", undefined, async () => {
|
||||
const h = installHarness([]);
|
||||
try {
|
||||
const { catalogCallbacks, ctx } = setupHarness({
|
||||
baseURL: "https://gw.example.com",
|
||||
providerId: "omniroute",
|
||||
apiKey: "chat-key",
|
||||
managementReadToken: "",
|
||||
});
|
||||
await runSetup(ctx);
|
||||
assert.equal(fallbackWarns(h.warns).length, 1);
|
||||
const { draft } = stubDraft();
|
||||
await catalogCallbacks[0](draft);
|
||||
assert.equal(h.seen.get(COMBOS_URL), "Bearer chat-key");
|
||||
} finally {
|
||||
h.restore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("enriches the catalog from the environment token alone", async () => {
|
||||
const providers = new Map<string, ProviderV2Info>();
|
||||
const models = new Map<string, ModelV2Info>();
|
||||
const draft = {
|
||||
provider: {
|
||||
list: () => [],
|
||||
get: (id: string) => providers.get(id) as never,
|
||||
update: (id: string, fn: (p: ProviderV2Info) => void) => {
|
||||
const p = (providers.get(id) ?? { id }) as ProviderV2Info;
|
||||
fn(p);
|
||||
providers.set(id, p);
|
||||
},
|
||||
remove: () => {},
|
||||
},
|
||||
model: {
|
||||
get: () => undefined,
|
||||
update: (pid: string, mid: string, fn: (m: ModelV2Info) => void) => {
|
||||
const k = pid + "/" + mid;
|
||||
const m = (models.get(k) ?? { id: mid, providerID: pid }) as ModelV2Info;
|
||||
fn(m);
|
||||
models.set(k, m);
|
||||
},
|
||||
remove: () => {},
|
||||
default: { get: () => undefined, set: () => {} },
|
||||
},
|
||||
} as unknown as CatalogDraft;
|
||||
let seenCombos = "";
|
||||
let seenPricing = "";
|
||||
const res = await withIsolatedEnv("mgmt-env-token", undefined, async () =>
|
||||
publishCatalog(
|
||||
draft,
|
||||
{
|
||||
providerId: "omniroute",
|
||||
baseURL: "https://gw.example.com",
|
||||
apiKey: "chat-key",
|
||||
managementReadToken: process.env[MGMT_ENV_VAR],
|
||||
timeoutMs: 1000,
|
||||
modelCacheTtlMs: 300000,
|
||||
usableOnly: false,
|
||||
},
|
||||
{
|
||||
fetcher: async () => [{ id: "m1" }],
|
||||
combosFetcher: async (_base, token) => {
|
||||
seenCombos = token;
|
||||
return [{ id: "team-combo", models: [{ kind: "model", model: "m1" }] }];
|
||||
},
|
||||
enrichmentFetcher: async (_base, token) => {
|
||||
seenPricing = token;
|
||||
// The process env is the source under test: the resolver output
|
||||
// flows in through the option above, so report success only when
|
||||
// the flow under test actually carried it.
|
||||
if (token !== "mgmt-env-token") return new Map();
|
||||
return new Map([["team-combo", { name: "Team Combo" }]]);
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
assert.deepEqual(res, { models: 1, combos: 1, autoCombos: 0 });
|
||||
assert.equal(seenCombos, "mgmt-env-token");
|
||||
assert.equal(seenPricing, "mgmt-env-token");
|
||||
const entry = models.get("omniroute/team-combo");
|
||||
assert.ok(entry, "expected the combo entry in the published catalog");
|
||||
assert.equal(entry?.name, "Team Combo");
|
||||
});
|
||||
});
|
||||
1
changelog.d/fixes/13613-management-token-env.md
Normal file
1
changelog.d/fixes/13613-management-token-env.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(opencode):** the v2 plugin reads the management token from OMNIROUTE_MANAGEMENT_API_KEY (plugin option wins) and warns once at startup when management calls fall back to the inference key ([#13613](https://github.com/diegosouzapw/OmniRoute/pull/13613)) — thanks @maxmad64bis
|
||||
Reference in New Issue
Block a user