From 9fb0474d1cef2894cba11a9c981180fedf9cd5e6 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Wed, 10 Jun 2026 16:04:22 -0500 Subject: [PATCH 1/7] feat(ovhcloud): add reasoning options --- packages/core/src/sync/providers/ovhcloud.ts | 1 + packages/core/test/ovhcloud-sync.test.ts | 32 +++++++++++++++++++ providers/ovhcloud/models/gpt-oss-120b.toml | 1 + providers/ovhcloud/models/gpt-oss-20b.toml | 1 + providers/ovhcloud/models/qwen3-32b.toml | 1 + .../ovhcloud/models/qwen3.5-397b-a17b.toml | 1 + providers/ovhcloud/models/qwen3.5-9b.toml | 1 + providers/ovhcloud/models/qwen3.6-27b.toml | 1 + sync.md | 1 + 9 files changed, 40 insertions(+) create mode 100644 packages/core/test/ovhcloud-sync.test.ts diff --git a/packages/core/src/sync/providers/ovhcloud.ts b/packages/core/src/sync/providers/ovhcloud.ts index ef6ffd7c2..0e18565e6 100644 --- a/packages/core/src/sync/providers/ovhcloud.ts +++ b/packages/core/src/sync/providers/ovhcloud.ts @@ -121,6 +121,7 @@ export function buildOvhcloudModel( last_updated: lastUpdated, attachment, reasoning, + reasoning_options: reasoning ? existing?.reasoning_options : undefined, temperature: temperature || undefined, tool_call: toolCall, structured_output: structuredOutput || undefined, diff --git a/packages/core/test/ovhcloud-sync.test.ts b/packages/core/test/ovhcloud-sync.test.ts new file mode 100644 index 000000000..a7f063087 --- /dev/null +++ b/packages/core/test/ovhcloud-sync.test.ts @@ -0,0 +1,32 @@ +import { expect, test } from "bun:test"; + +import { buildOvhcloudModel, type OvhcloudModel } from "../src/sync/providers/ovhcloud.js"; + +const model: OvhcloudModel = { + id: "Qwen3-32B", + name: "Qwen3-32B", + created: 1_752_655_628, + hugging_face_id: "Qwen/Qwen3-32B", + context_length: 32_768, + max_output_length: 32_768, + supported_features: ["reasoning"], +}; + +test("OVHcloud sync preserves authored reasoning options", () => { + const synced = buildOvhcloudModel(model, { + release_date: "2025-07-16", + last_updated: "2025-07-16", + reasoning_options: [{ type: "toggle" }], + }); + + expect(synced.reasoning_options).toEqual([{ type: "toggle" }]); +}); + +test("OVHcloud sync omits reasoning options for non-reasoning models", () => { + const synced = buildOvhcloudModel( + { ...model, supported_features: [] }, + { reasoning_options: [{ type: "toggle" }] }, + ); + + expect(synced.reasoning_options).toBeUndefined(); +}); diff --git a/providers/ovhcloud/models/gpt-oss-120b.toml b/providers/ovhcloud/models/gpt-oss-120b.toml index 3c762f2c1..d95b3ec53 100644 --- a/providers/ovhcloud/models/gpt-oss-120b.toml +++ b/providers/ovhcloud/models/gpt-oss-120b.toml @@ -3,6 +3,7 @@ release_date = "2025-08-28" last_updated = "2025-08-28" attachment = false reasoning = true +reasoning_options = [] tool_call = true structured_output = true open_weights = true diff --git a/providers/ovhcloud/models/gpt-oss-20b.toml b/providers/ovhcloud/models/gpt-oss-20b.toml index 71e3afc37..282ea7e8c 100644 --- a/providers/ovhcloud/models/gpt-oss-20b.toml +++ b/providers/ovhcloud/models/gpt-oss-20b.toml @@ -3,6 +3,7 @@ release_date = "2025-08-28" last_updated = "2025-08-28" attachment = false reasoning = true +reasoning_options = [] tool_call = true structured_output = true open_weights = true diff --git a/providers/ovhcloud/models/qwen3-32b.toml b/providers/ovhcloud/models/qwen3-32b.toml index 58ef5da29..ffdc77096 100644 --- a/providers/ovhcloud/models/qwen3-32b.toml +++ b/providers/ovhcloud/models/qwen3-32b.toml @@ -3,6 +3,7 @@ release_date = "2025-07-16" last_updated = "2025-07-16" attachment = false reasoning = true +reasoning_options = [{ type = "toggle" }] temperature = true tool_call = true structured_output = true diff --git a/providers/ovhcloud/models/qwen3.5-397b-a17b.toml b/providers/ovhcloud/models/qwen3.5-397b-a17b.toml index 9969e34dc..79f443d5c 100644 --- a/providers/ovhcloud/models/qwen3.5-397b-a17b.toml +++ b/providers/ovhcloud/models/qwen3.5-397b-a17b.toml @@ -3,6 +3,7 @@ release_date = "2026-05-18" last_updated = "2026-05-18" attachment = true reasoning = true +reasoning_options = [] temperature = true tool_call = true structured_output = true diff --git a/providers/ovhcloud/models/qwen3.5-9b.toml b/providers/ovhcloud/models/qwen3.5-9b.toml index a624b3126..993f35172 100644 --- a/providers/ovhcloud/models/qwen3.5-9b.toml +++ b/providers/ovhcloud/models/qwen3.5-9b.toml @@ -3,6 +3,7 @@ release_date = "2026-04-22" last_updated = "2026-04-22" attachment = true reasoning = true +reasoning_options = [] temperature = true tool_call = true structured_output = true diff --git a/providers/ovhcloud/models/qwen3.6-27b.toml b/providers/ovhcloud/models/qwen3.6-27b.toml index f8b47531b..8fc7ecc48 100644 --- a/providers/ovhcloud/models/qwen3.6-27b.toml +++ b/providers/ovhcloud/models/qwen3.6-27b.toml @@ -3,6 +3,7 @@ release_date = "2026-06-01" last_updated = "2026-06-01" attachment = true reasoning = true +reasoning_options = [] temperature = true tool_call = true structured_output = true diff --git a/sync.md b/sync.md index c07e9547f..dc58896ed 100644 --- a/sync.md +++ b/sync.md @@ -164,6 +164,7 @@ OVHcloud AI Endpoints is implemented in `packages/core/src/sync/providers/ovhclo - Model IDs are lowercased from the catalog `id` to match the existing TOML paths under `providers/ovhcloud/models`. - API prices are per-token strings and are converted to per-1M-token numbers; free models (price `0`) get no `[cost]` section. - `reasoning`, `tool_call`, and `structured_output` come from `supported_features`; `temperature` comes from `supported_sampling_parameters`. +- Authored `reasoning_options` are preserved for reasoning models. `Qwen3-32B` supports toggling reasoning through OVHcloud's documented `/no_think` prompt control; other reasoning models declare an empty array because OVHcloud documents no configurable control for them. - `attachment` is derived from non-text `input_modalities`, and `open_weights` from the presence of `hugging_face_id`. - `release_date`/`last_updated` default to the catalog `created` timestamp but preserve any existing hand-authored dates; `knowledge`, `family`, `status`, `interleaved`, and `limit.input` are preserved when present. From c4650219c390b86c5ba2012d617e33be2f7474fc Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Wed, 10 Jun 2026 19:50:42 -0500 Subject: [PATCH 2/7] fix(sync): drop stale reasoning options --- packages/core/src/sync/index.ts | 6 +++- packages/core/test/sync-runner.test.ts | 37 +++++++++++++++++++++ providers/ovhcloud/models/gpt-oss-120b.toml | 2 +- providers/ovhcloud/models/gpt-oss-20b.toml | 2 +- sync.md | 2 +- 5 files changed, 45 insertions(+), 4 deletions(-) diff --git a/packages/core/src/sync/index.ts b/packages/core/src/sync/index.ts index 4cb51a923..f46a2d030 100644 --- a/packages/core/src/sync/index.ts +++ b/packages/core/src/sync/index.ts @@ -320,7 +320,11 @@ export function preserveReasoningOptions( model: SyncedModel, existing: ExistingModel | undefined, ): SyncedModel { - if (model.reasoning_options !== undefined || existing?.reasoning_options === undefined) return model; + if ( + model.reasoning === false || + model.reasoning_options !== undefined || + existing?.reasoning_options === undefined + ) return model; return { ...model, reasoning_options: existing.reasoning_options, diff --git a/packages/core/test/sync-runner.test.ts b/packages/core/test/sync-runner.test.ts index 10c07969d..96844f9fc 100644 --- a/packages/core/test/sync-runner.test.ts +++ b/packages/core/test/sync-runner.test.ts @@ -172,6 +172,43 @@ output = ["text"] expect(second.unchanged).toBe(1); }); +test("sync removes authored reasoning options when a translator disables reasoning", async () => { + const { modelsDir } = await fixture(); + const filePath = path.join(modelsDir, "model.toml"); + await Bun.write(filePath, `name = "Old name" +release_date = "2026-01-01" +last_updated = "2026-01-01" +attachment = false +reasoning = true +reasoning_options = [{ type = "toggle" }] +tool_call = false +open_weights = false + +[cost] +input = 1 +output = 2 + +[limit] +context = 1000 +output = 100 + +[modalities] +input = ["text"] +output = ["text"] +`); + const sync = provider(modelsDir, ["model"]); + + const first = await syncProvider(sync); + const content = await Bun.file(filePath).text(); + const second = await syncProvider(sync); + + expect(first.updated).toBe(1); + expect(content).toContain("reasoning = false"); + expect(content).not.toContain("reasoning_options"); + expect(second.updated).toBe(0); + expect(second.unchanged).toBe(1); +}); + test("sync writes metadata returned by a provider translator", async () => { const root = await mkdtemp(path.join(os.tmpdir(), "models-dev-sync-metadata-")); const modelsDir = path.join(root, "providers", "test", "models"); diff --git a/providers/ovhcloud/models/gpt-oss-120b.toml b/providers/ovhcloud/models/gpt-oss-120b.toml index d95b3ec53..3a5e0a0df 100644 --- a/providers/ovhcloud/models/gpt-oss-120b.toml +++ b/providers/ovhcloud/models/gpt-oss-120b.toml @@ -3,7 +3,7 @@ release_date = "2025-08-28" last_updated = "2025-08-28" attachment = false reasoning = true -reasoning_options = [] +reasoning_options = [{ type = "effort", values = ["low", "medium", "high"] }] tool_call = true structured_output = true open_weights = true diff --git a/providers/ovhcloud/models/gpt-oss-20b.toml b/providers/ovhcloud/models/gpt-oss-20b.toml index 282ea7e8c..e2c154a2e 100644 --- a/providers/ovhcloud/models/gpt-oss-20b.toml +++ b/providers/ovhcloud/models/gpt-oss-20b.toml @@ -3,7 +3,7 @@ release_date = "2025-08-28" last_updated = "2025-08-28" attachment = false reasoning = true -reasoning_options = [] +reasoning_options = [{ type = "effort", values = ["low", "medium", "high"] }] tool_call = true structured_output = true open_weights = true diff --git a/sync.md b/sync.md index dc58896ed..089cc76a1 100644 --- a/sync.md +++ b/sync.md @@ -164,7 +164,7 @@ OVHcloud AI Endpoints is implemented in `packages/core/src/sync/providers/ovhclo - Model IDs are lowercased from the catalog `id` to match the existing TOML paths under `providers/ovhcloud/models`. - API prices are per-token strings and are converted to per-1M-token numbers; free models (price `0`) get no `[cost]` section. - `reasoning`, `tool_call`, and `structured_output` come from `supported_features`; `temperature` comes from `supported_sampling_parameters`. -- Authored `reasoning_options` are preserved for reasoning models. `Qwen3-32B` supports toggling reasoning through OVHcloud's documented `/no_think` prompt control; other reasoning models declare an empty array because OVHcloud documents no configurable control for them. +- Authored `reasoning_options` are preserved for reasoning models. `Qwen3-32B` supports toggling reasoning through OVHcloud's documented `/no_think` prompt control; both gpt-oss models support `low`, `medium`, and `high` reasoning effort; the remaining reasoning models declare an empty array because OVHcloud documents no configurable control for them. - `attachment` is derived from non-text `input_modalities`, and `open_weights` from the presence of `hugging_face_id`. - `release_date`/`last_updated` default to the catalog `created` timestamp but preserve any existing hand-authored dates; `knowledge`, `family`, `status`, `interleaved`, and `limit.input` are preserved when present. From 0e371b761d11cb184e2828e2d3b8dccd390e88b3 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Wed, 10 Jun 2026 23:14:13 -0500 Subject: [PATCH 3/7] fix(sync): resolve reasoning before preservation --- packages/core/src/sync/index.ts | 4 +- packages/core/test/sync-runner.test.ts | 56 ++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/packages/core/src/sync/index.ts b/packages/core/src/sync/index.ts index f46a2d030..9d7264bd7 100644 --- a/packages/core/src/sync/index.ts +++ b/packages/core/src/sync/index.ts @@ -170,6 +170,7 @@ export async function syncProvider( ? translated.model : preserveBaseModel(translated.model, existing.get(relativePath)?.authored), existing.get(relativePath)?.authored, + existing.get(relativePath)?.toml, ), })); if (!parsed.success) { @@ -319,9 +320,10 @@ export function preserveBaseModel(model: SyncedModel, existing: ExistingModel | export function preserveReasoningOptions( model: SyncedModel, existing: ExistingModel | undefined, + resolvedExisting: ExistingModel | undefined = existing, ): SyncedModel { if ( - model.reasoning === false || + (model.reasoning ?? resolvedExisting?.reasoning) === false || model.reasoning_options !== undefined || existing?.reasoning_options === undefined ) return model; diff --git a/packages/core/test/sync-runner.test.ts b/packages/core/test/sync-runner.test.ts index 96844f9fc..8e558cf49 100644 --- a/packages/core/test/sync-runner.test.ts +++ b/packages/core/test/sync-runner.test.ts @@ -3,6 +3,7 @@ import path from "node:path"; import { mkdtemp, mkdir, readlink, symlink } from "node:fs/promises"; import os from "node:os"; +import { generate } from "../src/index.js"; import { AuthoredModelShape } from "../src/schema.js"; import { syncProvider, type SyncProvider, type SyncedFullModel } from "../src/sync/index.js"; @@ -209,6 +210,61 @@ output = ["text"] expect(second.unchanged).toBe(1); }); +test("sync removes authored reasoning options inherited beside reasoning false", async () => { + const { root, modelsDir } = await fixture(); + const filePath = path.join(modelsDir, "model.toml"); + await Bun.write(path.join(root, "providers", "test", "provider.toml"), `name = "Test" +npm = "@ai-sdk/openai" +env = ["TEST_API_KEY"] +doc = "https://example.com/models" +`); + await mkdir(path.join(root, "models", "test"), { recursive: true }); + await Bun.write(path.join(root, "models", "test", "model.toml"), `name = "Test model" +release_date = "2026-01-01" +last_updated = "2026-01-01" +attachment = false +reasoning = false +tool_call = false +open_weights = false + +[limit] +context = 1000 +output = 100 + +[modalities] +input = ["text"] +output = ["text"] +`); + await Bun.write(filePath, `base_model = "test/model" +reasoning_options = [{ type = "toggle" }] + +[cost] +input = 1 +output = 2 +`); + const sync = provider(modelsDir, ["model"]); + sync.translateModel = (id) => ({ + id, + model: { + base_model: "test/model", + cost: { input: 1, output: 2 }, + }, + }); + + const first = await syncProvider(sync); + const content = await Bun.file(filePath).text(); + const generated = (await generate(path.join(root, "providers"))).test?.models.model; + const second = await syncProvider(sync); + + expect(first.updated).toBe(1); + expect(content).toContain('base_model = "test/model"'); + expect(content).not.toContain("reasoning_options"); + expect(generated?.reasoning).toBe(false); + expect(generated?.reasoning_options).toBeUndefined(); + expect(second.updated).toBe(0); + expect(second.unchanged).toBe(1); +}); + test("sync writes metadata returned by a provider translator", async () => { const root = await mkdtemp(path.join(os.tmpdir(), "models-dev-sync-metadata-")); const modelsDir = path.join(root, "providers", "test", "models"); From fc4a781c2ff70b46b892015a90679335baa5f514 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Wed, 10 Jun 2026 23:16:08 -0500 Subject: [PATCH 4/7] fix(ovhcloud): complete reasoning controls --- packages/core/src/sync/index.ts | 26 ++++++++++++------- packages/core/test/ovhcloud-sync.test.ts | 22 ++++++++++++++++ packages/core/test/sync-runner.test.ts | 8 ++++++ .../ovhcloud/models/qwen3.5-397b-a17b.toml | 2 +- providers/ovhcloud/models/qwen3.5-9b.toml | 2 +- providers/ovhcloud/models/qwen3.6-27b.toml | 2 +- sync.md | 2 +- 7 files changed, 50 insertions(+), 14 deletions(-) diff --git a/packages/core/src/sync/index.ts b/packages/core/src/sync/index.ts index 9d7264bd7..6526c7121 100644 --- a/packages/core/src/sync/index.ts +++ b/packages/core/src/sync/index.ts @@ -163,14 +163,20 @@ export async function syncProvider( }); } + const translatedModel = provider.preserveBaseModels === false + ? translated.model + : preserveBaseModel(translated.model, existing.get(relativePath)?.authored); + const resolvedReasoning = translated.metadata !== undefined && + "base_model" in translatedModel && + translated.metadata?.id === translatedModel.base_model + ? translated.metadata.model.reasoning + : existing.get(relativePath)?.toml.reasoning; const parsed = SyncedAuthoredModel.safeParse(stripUndefined({ id: translated.id, ...preserveReasoningOptions( - provider.preserveBaseModels === false - ? translated.model - : preserveBaseModel(translated.model, existing.get(relativePath)?.authored), + translatedModel, existing.get(relativePath)?.authored, - existing.get(relativePath)?.toml, + resolvedReasoning, ), })); if (!parsed.success) { @@ -320,13 +326,13 @@ export function preserveBaseModel(model: SyncedModel, existing: ExistingModel | export function preserveReasoningOptions( model: SyncedModel, existing: ExistingModel | undefined, - resolvedExisting: ExistingModel | undefined = existing, + resolvedReasoning: boolean | undefined = existing?.reasoning, ): SyncedModel { - if ( - (model.reasoning ?? resolvedExisting?.reasoning) === false || - model.reasoning_options !== undefined || - existing?.reasoning_options === undefined - ) return model; + if ((model.reasoning ?? resolvedReasoning) === false) { + const { reasoning_options: _reasoningOptions, ...withoutReasoningOptions } = model; + return withoutReasoningOptions as SyncedModel; + } + if (model.reasoning_options !== undefined || existing?.reasoning_options === undefined) return model; return { ...model, reasoning_options: existing.reasoning_options, diff --git a/packages/core/test/ovhcloud-sync.test.ts b/packages/core/test/ovhcloud-sync.test.ts index a7f063087..964fdcc11 100644 --- a/packages/core/test/ovhcloud-sync.test.ts +++ b/packages/core/test/ovhcloud-sync.test.ts @@ -1,4 +1,5 @@ import { expect, test } from "bun:test"; +import path from "node:path"; import { buildOvhcloudModel, type OvhcloudModel } from "../src/sync/providers/ovhcloud.js"; @@ -30,3 +31,24 @@ test("OVHcloud sync omits reasoning options for non-reasoning models", () => { expect(synced.reasoning_options).toBeUndefined(); }); + +test("OVHcloud reasoning models declare the exact provider control matrix", async () => { + const modelsDir = path.join(import.meta.dirname, "..", "..", "..", "providers", "ovhcloud", "models"); + const expected = { + "gpt-oss-20b": [{ type: "effort", values: ["low", "medium", "high"] }], + "gpt-oss-120b": [{ type: "effort", values: ["low", "medium", "high"] }], + "qwen3-32b": [{ type: "toggle" }], + "qwen3.5-9b": [{ type: "effort", values: ["none", "low", "medium", "high"] }], + "qwen3.5-397b-a17b": [{ type: "effort", values: ["none", "low", "medium", "high"] }], + "qwen3.6-27b": [{ type: "effort", values: ["none", "minimal", "low", "medium", "high"] }], + }; + + for (const [id, reasoningOptions] of Object.entries(expected)) { + const authored = Bun.TOML.parse(await Bun.file(path.join(modelsDir, `${id}.toml`)).text()) as { + reasoning?: boolean; + reasoning_options?: unknown; + }; + expect(authored.reasoning, id).toBe(true); + expect(authored.reasoning_options, id).toEqual(reasoningOptions); + } +}); diff --git a/packages/core/test/sync-runner.test.ts b/packages/core/test/sync-runner.test.ts index 8e558cf49..97680261c 100644 --- a/packages/core/test/sync-runner.test.ts +++ b/packages/core/test/sync-runner.test.ts @@ -198,6 +198,14 @@ input = ["text"] output = ["text"] `); const sync = provider(modelsDir, ["model"]); + sync.translateModel = (id) => ({ + id, + model: { + ...model, + reasoning: false, + reasoning_options: [{ type: "toggle" }], + }, + }); const first = await syncProvider(sync); const content = await Bun.file(filePath).text(); diff --git a/providers/ovhcloud/models/qwen3.5-397b-a17b.toml b/providers/ovhcloud/models/qwen3.5-397b-a17b.toml index 79f443d5c..00ff86a11 100644 --- a/providers/ovhcloud/models/qwen3.5-397b-a17b.toml +++ b/providers/ovhcloud/models/qwen3.5-397b-a17b.toml @@ -3,7 +3,7 @@ release_date = "2026-05-18" last_updated = "2026-05-18" attachment = true reasoning = true -reasoning_options = [] +reasoning_options = [{ type = "effort", values = ["none", "low", "medium", "high"] }] temperature = true tool_call = true structured_output = true diff --git a/providers/ovhcloud/models/qwen3.5-9b.toml b/providers/ovhcloud/models/qwen3.5-9b.toml index 993f35172..4fe9ba0ea 100644 --- a/providers/ovhcloud/models/qwen3.5-9b.toml +++ b/providers/ovhcloud/models/qwen3.5-9b.toml @@ -3,7 +3,7 @@ release_date = "2026-04-22" last_updated = "2026-04-22" attachment = true reasoning = true -reasoning_options = [] +reasoning_options = [{ type = "effort", values = ["none", "low", "medium", "high"] }] temperature = true tool_call = true structured_output = true diff --git a/providers/ovhcloud/models/qwen3.6-27b.toml b/providers/ovhcloud/models/qwen3.6-27b.toml index 8fc7ecc48..83a24178d 100644 --- a/providers/ovhcloud/models/qwen3.6-27b.toml +++ b/providers/ovhcloud/models/qwen3.6-27b.toml @@ -3,7 +3,7 @@ release_date = "2026-06-01" last_updated = "2026-06-01" attachment = true reasoning = true -reasoning_options = [] +reasoning_options = [{ type = "effort", values = ["none", "minimal", "low", "medium", "high"] }] temperature = true tool_call = true structured_output = true diff --git a/sync.md b/sync.md index 089cc76a1..78cd8bbd9 100644 --- a/sync.md +++ b/sync.md @@ -164,7 +164,7 @@ OVHcloud AI Endpoints is implemented in `packages/core/src/sync/providers/ovhclo - Model IDs are lowercased from the catalog `id` to match the existing TOML paths under `providers/ovhcloud/models`. - API prices are per-token strings and are converted to per-1M-token numbers; free models (price `0`) get no `[cost]` section. - `reasoning`, `tool_call`, and `structured_output` come from `supported_features`; `temperature` comes from `supported_sampling_parameters`. -- Authored `reasoning_options` are preserved for reasoning models. `Qwen3-32B` supports toggling reasoning through OVHcloud's documented `/no_think` prompt control; both gpt-oss models support `low`, `medium`, and `high` reasoning effort; the remaining reasoning models declare an empty array because OVHcloud documents no configurable control for them. +- Authored `reasoning_options` are preserved for reasoning models. `Qwen3-32B` supports toggling reasoning through OVHcloud's documented `/no_think` prompt control. Both gpt-oss models support `low`, `medium`, and `high` reasoning effort. The Qwen3.5 models support `none`, `low`, `medium`, and `high`; Qwen3.6-27B additionally supports `minimal`. - `attachment` is derived from non-text `input_modalities`, and `open_weights` from the presence of `hugging_face_id`. - `release_date`/`last_updated` default to the catalog `created` timestamp but preserve any existing hand-authored dates; `knowledge`, `family`, `status`, `interleaved`, and `limit.input` are preserved when present. From 20bfe37a538d50c0fba20136e3d3b704974a24ba Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Wed, 10 Jun 2026 23:19:32 -0500 Subject: [PATCH 5/7] fix(sync): resolve changed canonical base --- packages/core/src/sync/index.ts | 24 +++++++--- packages/core/test/sync-runner.test.ts | 64 ++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 7 deletions(-) diff --git a/packages/core/src/sync/index.ts b/packages/core/src/sync/index.ts index 6526c7121..5251666de 100644 --- a/packages/core/src/sync/index.ts +++ b/packages/core/src/sync/index.ts @@ -119,7 +119,9 @@ export async function syncProvider( ): Promise { console.log(`\nSyncing ${provider.name}...`); - const { models: existing, brokenSymlinks } = await readExisting(provider.modelsDir); + const existingState = await readExisting(provider.modelsDir); + const { models: existing, brokenSymlinks } = existingState; + let { modelMetadata } = existingState; const sourceModels = provider.parseModels(await provider.fetchModels()); const desired = new Map; content: string }>(); const desiredMetadata = new Map; content: string }>(); @@ -166,11 +168,19 @@ export async function syncProvider( const translatedModel = provider.preserveBaseModels === false ? translated.model : preserveBaseModel(translated.model, existing.get(relativePath)?.authored); - const resolvedReasoning = translated.metadata !== undefined && - "base_model" in translatedModel && - translated.metadata?.id === translatedModel.base_model - ? translated.metadata.model.reasoning - : existing.get(relativePath)?.toml.reasoning; + const translatedBase = "base_model" in translatedModel ? translatedModel.base_model : undefined; + let resolvedReasoning: boolean | undefined; + if (translatedBase !== undefined) { + if (translated.metadata?.id === translatedBase) { + resolvedReasoning = translated.metadata.model.reasoning; + } else { + modelMetadata ??= await readModelMetadata(provider.modelsDir); + const canonicalReasoning = modelMetadata[translatedBase]?.reasoning; + resolvedReasoning = typeof canonicalReasoning === "boolean" ? canonicalReasoning : undefined; + } + } else { + resolvedReasoning = existing.get(relativePath)?.toml.reasoning; + } const parsed = SyncedAuthoredModel.safeParse(stripUndefined({ id: translated.id, ...preserveReasoningOptions( @@ -404,7 +414,7 @@ async function readExisting(modelsDir: string) { existing.set(file, { authored, toml, symlink }); } - return { models: existing, brokenSymlinks }; + return { models: existing, brokenSymlinks, modelMetadata }; } async function isSymlink(filePath: string) { diff --git a/packages/core/test/sync-runner.test.ts b/packages/core/test/sync-runner.test.ts index 97680261c..9f2bbf834 100644 --- a/packages/core/test/sync-runner.test.ts +++ b/packages/core/test/sync-runner.test.ts @@ -273,6 +273,70 @@ output = 2 expect(second.unchanged).toBe(1); }); +test("sync removes authored reasoning options when changing to a non-reasoning base", async () => { + const { root, modelsDir } = await fixture(); + const metadataDir = path.join(root, "models", "test"); + const filePath = path.join(modelsDir, "model.toml"); + await mkdir(metadataDir, { recursive: true }); + await Bun.write(path.join(metadataDir, "reasoning.toml"), `name = "Reasoning base" +release_date = "2026-01-01" +last_updated = "2026-01-01" +attachment = false +reasoning = true +tool_call = false +open_weights = false + +[limit] +context = 1000 +output = 100 + +[modalities] +input = ["text"] +output = ["text"] +`); + await Bun.write(path.join(metadataDir, "standard.toml"), `name = "Standard base" +release_date = "2026-01-01" +last_updated = "2026-01-01" +attachment = false +reasoning = false +tool_call = false +open_weights = false + +[limit] +context = 1000 +output = 100 + +[modalities] +input = ["text"] +output = ["text"] +`); + await Bun.write(filePath, `base_model = "test/reasoning" +reasoning_options = [{ type = "toggle" }] + +[cost] +input = 1 +output = 2 +`); + const sync = provider(modelsDir, ["model"]); + sync.translateModel = (id) => ({ + id, + model: { + base_model: "test/standard", + cost: { input: 1, output: 2 }, + }, + }); + + const first = await syncProvider(sync); + const content = await Bun.file(filePath).text(); + const second = await syncProvider(sync); + + expect(first.updated).toBe(1); + expect(content).toContain('base_model = "test/standard"'); + expect(content).not.toContain("reasoning_options"); + expect(second.updated).toBe(0); + expect(second.unchanged).toBe(1); +}); + test("sync writes metadata returned by a provider translator", async () => { const root = await mkdtemp(path.join(os.tmpdir(), "models-dev-sync-metadata-")); const modelsDir = path.join(root, "providers", "test", "models"); From 21581d8f4c9a99d4fb9b317f1a230b7d5a312e54 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Wed, 10 Jun 2026 23:20:55 -0500 Subject: [PATCH 6/7] test(sync): cover factored reasoning overrides --- packages/core/test/sync-runner.test.ts | 97 ++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/packages/core/test/sync-runner.test.ts b/packages/core/test/sync-runner.test.ts index 9f2bbf834..075ef8bf3 100644 --- a/packages/core/test/sync-runner.test.ts +++ b/packages/core/test/sync-runner.test.ts @@ -49,6 +49,34 @@ async function fixture() { return { root, modelsDir }; } +async function factoredFixture(reasoning: boolean) { + const result = await fixture(); + const metadataDir = path.join(result.root, "models", "test"); + await mkdir(metadataDir, { recursive: true }); + await Bun.write(path.join(result.root, "providers", "test", "provider.toml"), `name = "Test" +npm = "@ai-sdk/openai" +env = ["TEST_API_KEY"] +doc = "https://example.com/models" +`); + await Bun.write(path.join(metadataDir, "model.toml"), `name = "Canonical model" +release_date = "2026-01-01" +last_updated = "2026-01-01" +attachment = false +reasoning = ${reasoning} +tool_call = false +open_weights = false + +[limit] +context = 1000 +output = 100 + +[modalities] +input = ["text"] +output = ["text"] +`); + return result; +} + function provider( modelsDir: string, ids: string[], @@ -337,6 +365,75 @@ output = 2 expect(second.unchanged).toBe(1); }); +test("sync removes a reasoning override and options over a non-reasoning base", async () => { + const { root, modelsDir } = await factoredFixture(false); + const filePath = path.join(modelsDir, "model.toml"); + await Bun.write(filePath, `base_model = "test/model" +reasoning = true +reasoning_options = [{ type = "toggle" }] + +[cost] +input = 1 +output = 2 +`); + const sync = provider(modelsDir, ["model"]); + sync.translateModel = (id) => ({ + id, + model: { + base_model: "test/model", + cost: { input: 1, output: 2 }, + }, + }); + + const first = await syncProvider(sync); + const content = await Bun.file(filePath).text(); + const generated = (await generate(path.join(root, "providers"))).test?.models.model; + const second = await syncProvider(sync); + + expect(first.updated).toBe(1); + expect(content).not.toContain("reasoning ="); + expect(content).not.toContain("reasoning_options"); + expect(generated?.reasoning).toBe(false); + expect(generated?.reasoning_options).toBeUndefined(); + expect(second.updated).toBe(0); + expect(second.unchanged).toBe(1); +}); + +test("sync preserves options when removing a non-reasoning override over a reasoning base", async () => { + const { root, modelsDir } = await factoredFixture(true); + const filePath = path.join(modelsDir, "model.toml"); + await Bun.write(filePath, `base_model = "test/model" +reasoning = false +reasoning_options = [{ type = "toggle" }] + +[cost] +input = 1 +output = 2 +`); + const sync = provider(modelsDir, ["model"]); + sync.translateModel = (id) => ({ + id, + model: { + base_model: "test/model", + cost: { input: 1, output: 2 }, + }, + }); + + const first = await syncProvider(sync); + const content = await Bun.file(filePath).text(); + const generated = (await generate(path.join(root, "providers"))).test?.models.model; + const second = await syncProvider(sync); + + expect(first.updated).toBe(1); + expect(content).not.toContain("reasoning = false"); + expect(content).toContain("[[reasoning_options]]"); + expect(content).toContain('type = "toggle"'); + expect(generated?.reasoning).toBe(true); + expect(generated?.reasoning_options).toEqual([{ type: "toggle" }]); + expect(second.updated).toBe(0); + expect(second.unchanged).toBe(1); +}); + test("sync writes metadata returned by a provider translator", async () => { const root = await mkdtemp(path.join(os.tmpdir(), "models-dev-sync-metadata-")); const modelsDir = path.join(root, "providers", "test", "models"); From 9df50e0cccb53f6638bb9a661b02957c111ecf9e Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Thu, 11 Jun 2026 12:00:37 -0500 Subject: [PATCH 7/7] chore(ovhcloud): remove test changes --- packages/core/test/ovhcloud-sync.test.ts | 54 ----- packages/core/test/sync-runner.test.ts | 262 ----------------------- 2 files changed, 316 deletions(-) delete mode 100644 packages/core/test/ovhcloud-sync.test.ts diff --git a/packages/core/test/ovhcloud-sync.test.ts b/packages/core/test/ovhcloud-sync.test.ts deleted file mode 100644 index 964fdcc11..000000000 --- a/packages/core/test/ovhcloud-sync.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { expect, test } from "bun:test"; -import path from "node:path"; - -import { buildOvhcloudModel, type OvhcloudModel } from "../src/sync/providers/ovhcloud.js"; - -const model: OvhcloudModel = { - id: "Qwen3-32B", - name: "Qwen3-32B", - created: 1_752_655_628, - hugging_face_id: "Qwen/Qwen3-32B", - context_length: 32_768, - max_output_length: 32_768, - supported_features: ["reasoning"], -}; - -test("OVHcloud sync preserves authored reasoning options", () => { - const synced = buildOvhcloudModel(model, { - release_date: "2025-07-16", - last_updated: "2025-07-16", - reasoning_options: [{ type: "toggle" }], - }); - - expect(synced.reasoning_options).toEqual([{ type: "toggle" }]); -}); - -test("OVHcloud sync omits reasoning options for non-reasoning models", () => { - const synced = buildOvhcloudModel( - { ...model, supported_features: [] }, - { reasoning_options: [{ type: "toggle" }] }, - ); - - expect(synced.reasoning_options).toBeUndefined(); -}); - -test("OVHcloud reasoning models declare the exact provider control matrix", async () => { - const modelsDir = path.join(import.meta.dirname, "..", "..", "..", "providers", "ovhcloud", "models"); - const expected = { - "gpt-oss-20b": [{ type: "effort", values: ["low", "medium", "high"] }], - "gpt-oss-120b": [{ type: "effort", values: ["low", "medium", "high"] }], - "qwen3-32b": [{ type: "toggle" }], - "qwen3.5-9b": [{ type: "effort", values: ["none", "low", "medium", "high"] }], - "qwen3.5-397b-a17b": [{ type: "effort", values: ["none", "low", "medium", "high"] }], - "qwen3.6-27b": [{ type: "effort", values: ["none", "minimal", "low", "medium", "high"] }], - }; - - for (const [id, reasoningOptions] of Object.entries(expected)) { - const authored = Bun.TOML.parse(await Bun.file(path.join(modelsDir, `${id}.toml`)).text()) as { - reasoning?: boolean; - reasoning_options?: unknown; - }; - expect(authored.reasoning, id).toBe(true); - expect(authored.reasoning_options, id).toEqual(reasoningOptions); - } -}); diff --git a/packages/core/test/sync-runner.test.ts b/packages/core/test/sync-runner.test.ts index 075ef8bf3..10c07969d 100644 --- a/packages/core/test/sync-runner.test.ts +++ b/packages/core/test/sync-runner.test.ts @@ -3,7 +3,6 @@ import path from "node:path"; import { mkdtemp, mkdir, readlink, symlink } from "node:fs/promises"; import os from "node:os"; -import { generate } from "../src/index.js"; import { AuthoredModelShape } from "../src/schema.js"; import { syncProvider, type SyncProvider, type SyncedFullModel } from "../src/sync/index.js"; @@ -49,34 +48,6 @@ async function fixture() { return { root, modelsDir }; } -async function factoredFixture(reasoning: boolean) { - const result = await fixture(); - const metadataDir = path.join(result.root, "models", "test"); - await mkdir(metadataDir, { recursive: true }); - await Bun.write(path.join(result.root, "providers", "test", "provider.toml"), `name = "Test" -npm = "@ai-sdk/openai" -env = ["TEST_API_KEY"] -doc = "https://example.com/models" -`); - await Bun.write(path.join(metadataDir, "model.toml"), `name = "Canonical model" -release_date = "2026-01-01" -last_updated = "2026-01-01" -attachment = false -reasoning = ${reasoning} -tool_call = false -open_weights = false - -[limit] -context = 1000 -output = 100 - -[modalities] -input = ["text"] -output = ["text"] -`); - return result; -} - function provider( modelsDir: string, ids: string[], @@ -201,239 +172,6 @@ output = ["text"] expect(second.unchanged).toBe(1); }); -test("sync removes authored reasoning options when a translator disables reasoning", async () => { - const { modelsDir } = await fixture(); - const filePath = path.join(modelsDir, "model.toml"); - await Bun.write(filePath, `name = "Old name" -release_date = "2026-01-01" -last_updated = "2026-01-01" -attachment = false -reasoning = true -reasoning_options = [{ type = "toggle" }] -tool_call = false -open_weights = false - -[cost] -input = 1 -output = 2 - -[limit] -context = 1000 -output = 100 - -[modalities] -input = ["text"] -output = ["text"] -`); - const sync = provider(modelsDir, ["model"]); - sync.translateModel = (id) => ({ - id, - model: { - ...model, - reasoning: false, - reasoning_options: [{ type: "toggle" }], - }, - }); - - const first = await syncProvider(sync); - const content = await Bun.file(filePath).text(); - const second = await syncProvider(sync); - - expect(first.updated).toBe(1); - expect(content).toContain("reasoning = false"); - expect(content).not.toContain("reasoning_options"); - expect(second.updated).toBe(0); - expect(second.unchanged).toBe(1); -}); - -test("sync removes authored reasoning options inherited beside reasoning false", async () => { - const { root, modelsDir } = await fixture(); - const filePath = path.join(modelsDir, "model.toml"); - await Bun.write(path.join(root, "providers", "test", "provider.toml"), `name = "Test" -npm = "@ai-sdk/openai" -env = ["TEST_API_KEY"] -doc = "https://example.com/models" -`); - await mkdir(path.join(root, "models", "test"), { recursive: true }); - await Bun.write(path.join(root, "models", "test", "model.toml"), `name = "Test model" -release_date = "2026-01-01" -last_updated = "2026-01-01" -attachment = false -reasoning = false -tool_call = false -open_weights = false - -[limit] -context = 1000 -output = 100 - -[modalities] -input = ["text"] -output = ["text"] -`); - await Bun.write(filePath, `base_model = "test/model" -reasoning_options = [{ type = "toggle" }] - -[cost] -input = 1 -output = 2 -`); - const sync = provider(modelsDir, ["model"]); - sync.translateModel = (id) => ({ - id, - model: { - base_model: "test/model", - cost: { input: 1, output: 2 }, - }, - }); - - const first = await syncProvider(sync); - const content = await Bun.file(filePath).text(); - const generated = (await generate(path.join(root, "providers"))).test?.models.model; - const second = await syncProvider(sync); - - expect(first.updated).toBe(1); - expect(content).toContain('base_model = "test/model"'); - expect(content).not.toContain("reasoning_options"); - expect(generated?.reasoning).toBe(false); - expect(generated?.reasoning_options).toBeUndefined(); - expect(second.updated).toBe(0); - expect(second.unchanged).toBe(1); -}); - -test("sync removes authored reasoning options when changing to a non-reasoning base", async () => { - const { root, modelsDir } = await fixture(); - const metadataDir = path.join(root, "models", "test"); - const filePath = path.join(modelsDir, "model.toml"); - await mkdir(metadataDir, { recursive: true }); - await Bun.write(path.join(metadataDir, "reasoning.toml"), `name = "Reasoning base" -release_date = "2026-01-01" -last_updated = "2026-01-01" -attachment = false -reasoning = true -tool_call = false -open_weights = false - -[limit] -context = 1000 -output = 100 - -[modalities] -input = ["text"] -output = ["text"] -`); - await Bun.write(path.join(metadataDir, "standard.toml"), `name = "Standard base" -release_date = "2026-01-01" -last_updated = "2026-01-01" -attachment = false -reasoning = false -tool_call = false -open_weights = false - -[limit] -context = 1000 -output = 100 - -[modalities] -input = ["text"] -output = ["text"] -`); - await Bun.write(filePath, `base_model = "test/reasoning" -reasoning_options = [{ type = "toggle" }] - -[cost] -input = 1 -output = 2 -`); - const sync = provider(modelsDir, ["model"]); - sync.translateModel = (id) => ({ - id, - model: { - base_model: "test/standard", - cost: { input: 1, output: 2 }, - }, - }); - - const first = await syncProvider(sync); - const content = await Bun.file(filePath).text(); - const second = await syncProvider(sync); - - expect(first.updated).toBe(1); - expect(content).toContain('base_model = "test/standard"'); - expect(content).not.toContain("reasoning_options"); - expect(second.updated).toBe(0); - expect(second.unchanged).toBe(1); -}); - -test("sync removes a reasoning override and options over a non-reasoning base", async () => { - const { root, modelsDir } = await factoredFixture(false); - const filePath = path.join(modelsDir, "model.toml"); - await Bun.write(filePath, `base_model = "test/model" -reasoning = true -reasoning_options = [{ type = "toggle" }] - -[cost] -input = 1 -output = 2 -`); - const sync = provider(modelsDir, ["model"]); - sync.translateModel = (id) => ({ - id, - model: { - base_model: "test/model", - cost: { input: 1, output: 2 }, - }, - }); - - const first = await syncProvider(sync); - const content = await Bun.file(filePath).text(); - const generated = (await generate(path.join(root, "providers"))).test?.models.model; - const second = await syncProvider(sync); - - expect(first.updated).toBe(1); - expect(content).not.toContain("reasoning ="); - expect(content).not.toContain("reasoning_options"); - expect(generated?.reasoning).toBe(false); - expect(generated?.reasoning_options).toBeUndefined(); - expect(second.updated).toBe(0); - expect(second.unchanged).toBe(1); -}); - -test("sync preserves options when removing a non-reasoning override over a reasoning base", async () => { - const { root, modelsDir } = await factoredFixture(true); - const filePath = path.join(modelsDir, "model.toml"); - await Bun.write(filePath, `base_model = "test/model" -reasoning = false -reasoning_options = [{ type = "toggle" }] - -[cost] -input = 1 -output = 2 -`); - const sync = provider(modelsDir, ["model"]); - sync.translateModel = (id) => ({ - id, - model: { - base_model: "test/model", - cost: { input: 1, output: 2 }, - }, - }); - - const first = await syncProvider(sync); - const content = await Bun.file(filePath).text(); - const generated = (await generate(path.join(root, "providers"))).test?.models.model; - const second = await syncProvider(sync); - - expect(first.updated).toBe(1); - expect(content).not.toContain("reasoning = false"); - expect(content).toContain("[[reasoning_options]]"); - expect(content).toContain('type = "toggle"'); - expect(generated?.reasoning).toBe(true); - expect(generated?.reasoning_options).toEqual([{ type: "toggle" }]); - expect(second.updated).toBe(0); - expect(second.unchanged).toBe(1); -}); - test("sync writes metadata returned by a provider translator", async () => { const root = await mkdtemp(path.join(os.tmpdir(), "models-dev-sync-metadata-")); const modelsDir = path.join(root, "providers", "test", "models");