mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-22 07:02:16 +03:00
Compare commits
1 Commits
release/v3
...
fix/10940-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6d8e6ee38 |
1
changelog.d/fixes/10940-opencode-limit-output.md
Normal file
1
changelog.d/fixes/10940-opencode-limit-output.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
- fix(cli): always emit limit.output in generated OpenCode config so schema validation passes for metadata-less models (#10940)
|
||||||
@@ -243,9 +243,11 @@ function resolveContextLength(entry: CatalogModelEntry): number | undefined {
|
|||||||
* 1. Existing manual override in the user's opencode.json (`limit.context`).
|
* 1. Existing manual override in the user's opencode.json (`limit.context`).
|
||||||
* 2. Catalog `context_length` / `max_context_window_tokens`.
|
* 2. Catalog `context_length` / `max_context_window_tokens`.
|
||||||
*
|
*
|
||||||
* If neither is available, the entry is returned WITHOUT a `limit` block so
|
* If neither is available, `limit.context` is simply omitted and OpenCode's
|
||||||
* the caller can decide whether to skip the model entirely or surface a
|
* own heuristics apply — we never fabricate a default context window. The
|
||||||
* warning. We never fabricate a default context window.
|
* entry ALWAYS carries a `limit` block, though: `limit.output` is a
|
||||||
|
* required field in OpenCode's v1 provider schema, so it is always emitted
|
||||||
|
* (falling back to 8K when nothing else is known) — see #10940.
|
||||||
*/
|
*/
|
||||||
function buildModelEntry(
|
function buildModelEntry(
|
||||||
id: string,
|
id: string,
|
||||||
@@ -301,25 +303,23 @@ function buildModelEntry(
|
|||||||
const output =
|
const output =
|
||||||
typeof userOutput === "number" && userOutput > 0 ? userOutput : (catalogOutput ?? 8_192);
|
typeof userOutput === "number" && userOutput > 0 ? userOutput : (catalogOutput ?? 8_192);
|
||||||
|
|
||||||
// Emit `limit` only if we have at least one of context/output. We never
|
// `limit.output` is REQUIRED by OpenCode's v1 provider schema regardless of
|
||||||
// emit a half-baked limit block with only an `output` (would be misleading).
|
// whether the catalog (or the user's existing config) knows the model's
|
||||||
if (
|
// context window — a model with no catalog metadata at all must still get
|
||||||
typeof context === "number" ||
|
// a `limit` block, or OpenCode rejects the whole config with "Missing key
|
||||||
typeof userOutput === "number" ||
|
// provider.omniroute.models.{model}.limit.output" (#10940). `output` above
|
||||||
typeof catalogOutput === "number"
|
// already resolves to a safe fallback (8K) when nothing else is known, so
|
||||||
) {
|
// we always emit it; `context`/`input` are added only when actually known.
|
||||||
const limit: { context?: number; input?: number; output?: number } = {};
|
const limit: { context?: number; input?: number; output?: number } = { output };
|
||||||
if (typeof context === "number") limit.context = context;
|
if (typeof context === "number") limit.context = context;
|
||||||
limit.output = output;
|
const userInput = existing?.limit?.input;
|
||||||
const userInput = existing?.limit?.input;
|
if (typeof userInput === "number" && userInput > 0) {
|
||||||
if (typeof userInput === "number" && userInput > 0) {
|
limit.input = userInput;
|
||||||
limit.input = userInput;
|
} else if (catalog) {
|
||||||
} else if (catalog) {
|
const maxInput = catalog.max_input_tokens;
|
||||||
const maxInput = catalog.max_input_tokens;
|
if (typeof maxInput === "number" && maxInput > 0) limit.input = maxInput;
|
||||||
if (typeof maxInput === "number" && maxInput > 0) limit.input = maxInput;
|
|
||||||
}
|
|
||||||
entry.limit = limit;
|
|
||||||
}
|
}
|
||||||
|
entry.limit = limit;
|
||||||
|
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -603,12 +603,15 @@ describe("config-generator", () => {
|
|||||||
input: 100000,
|
input: 100000,
|
||||||
output: 32768,
|
output: 32768,
|
||||||
});
|
});
|
||||||
assert.strictEqual(models["no-metadata"].limit, undefined);
|
// #10940: `limit.output` is REQUIRED by OpenCode's v1 provider schema,
|
||||||
|
// so even a model with zero catalog metadata still gets a `limit`
|
||||||
|
// block carrying the fallback output value; `context`/`input` stay
|
||||||
|
// omitted since neither the catalog nor the user knows them.
|
||||||
|
assert.deepStrictEqual(models["no-metadata"].limit, { output: 8192 });
|
||||||
|
|
||||||
for (const model of Object.values(models) as Array<{ limit?: { output?: number } }>) {
|
for (const model of Object.values(models) as Array<{ limit?: { output?: number } }>) {
|
||||||
assert.ok(
|
assert.ok(
|
||||||
model.limit === undefined ||
|
typeof model.limit?.output === "number" && model.limit.output > 0,
|
||||||
(typeof model.limit.output === "number" && model.limit.output > 0),
|
|
||||||
"every emitted limit must contain a positive output"
|
"every emitted limit must contain a positive output"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
85
tests/unit/opencode-limit-output-10940.test.ts
Normal file
85
tests/unit/opencode-limit-output-10940.test.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { describe, it } from "node:test";
|
||||||
|
import assert from "node:assert";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regression guard for #10940: OpenCode rejects a generated config with
|
||||||
|
* "Missing key provider.omniroute.models.{model}.limit.output" whenever a
|
||||||
|
* model has no catalog metadata (no `context_length`, no
|
||||||
|
* `max_output_tokens`) and no existing user override. `limit.output` is a
|
||||||
|
* REQUIRED field in OpenCode's v1 provider schema, so it must always be
|
||||||
|
* emitted — even when nothing is known about the model.
|
||||||
|
*/
|
||||||
|
describe("opencode config generator — limit.output always emitted (#10940)", () => {
|
||||||
|
function makeCatalogResponse(models: unknown[]): unknown {
|
||||||
|
return { object: "list", data: models };
|
||||||
|
}
|
||||||
|
|
||||||
|
function stubFetchOnce(body: unknown, status = 200) {
|
||||||
|
const original = globalThis.fetch;
|
||||||
|
// @ts-ignore — globalThis.fetch signature is compatible for our purposes
|
||||||
|
globalThis.fetch = (async () => {
|
||||||
|
return new Response(JSON.stringify(body), {
|
||||||
|
status,
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
});
|
||||||
|
}) as typeof fetch;
|
||||||
|
return {
|
||||||
|
restore: () => {
|
||||||
|
globalThis.fetch = original;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
it("RED-proving case: a model with NO context_length and NO max_output_tokens still gets a numeric limit.output", async () => {
|
||||||
|
// This model has no metadata whatsoever beyond its id — exactly the
|
||||||
|
// shape that used to leave `entry.limit` undefined entirely (issue #10940).
|
||||||
|
const stub = stubFetchOnce(
|
||||||
|
makeCatalogResponse([{ id: "metadataless-model", owned_by: "someProvider" }])
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
const { generateOpencodeConfig } = await import(
|
||||||
|
"../../src/lib/cli-helper/config-generator/opencode.ts"
|
||||||
|
);
|
||||||
|
const out = await generateOpencodeConfig({
|
||||||
|
baseUrl: "http://localhost:20128",
|
||||||
|
apiKey: "sk-test",
|
||||||
|
});
|
||||||
|
const cfg = JSON.parse(out);
|
||||||
|
const entry = cfg.provider.omniroute.models["metadataless-model"];
|
||||||
|
assert.ok(entry, "model entry must exist in the generated config");
|
||||||
|
assert.ok(entry.limit, "entry.limit must be present even without catalog metadata");
|
||||||
|
assert.strictEqual(
|
||||||
|
typeof entry.limit.output,
|
||||||
|
"number",
|
||||||
|
`entry.limit.output must be a number, got ${JSON.stringify(entry.limit?.output)}`
|
||||||
|
);
|
||||||
|
assert.ok(entry.limit.output > 0, "entry.limit.output must be a positive number");
|
||||||
|
// context stays unknown — we must NOT fabricate it.
|
||||||
|
assert.strictEqual(entry.limit.context, undefined);
|
||||||
|
} finally {
|
||||||
|
stub.restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("honors the catalog's max_output_tokens when present", async () => {
|
||||||
|
const stub = stubFetchOnce(
|
||||||
|
makeCatalogResponse([
|
||||||
|
{ id: "has-output-meta", owned_by: "someProvider", max_output_tokens: 4096 },
|
||||||
|
])
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
const { generateOpencodeConfig } = await import(
|
||||||
|
"../../src/lib/cli-helper/config-generator/opencode.ts"
|
||||||
|
);
|
||||||
|
const out = await generateOpencodeConfig({
|
||||||
|
baseUrl: "http://localhost:20128",
|
||||||
|
apiKey: "sk-test",
|
||||||
|
});
|
||||||
|
const cfg = JSON.parse(out);
|
||||||
|
const entry = cfg.provider.omniroute.models["has-output-meta"];
|
||||||
|
assert.strictEqual(entry.limit.output, 4096);
|
||||||
|
} finally {
|
||||||
|
stub.restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user