Skip to content

Commit 7baee6e

Browse files
committed
fix(providers): address SambaNova and Nebius review feedback
1 parent d9c72e2 commit 7baee6e

5 files changed

Lines changed: 104 additions & 19 deletions

File tree

docs-site/src/content/docs/ja/getting-started/quickstart.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@ ocx init
1313

1414
`ocx init` では次の手順を説明します。
1515

16-
1. **プロバイダーを選択してください** — 71 の組み込みレジストリ プリセットのいずれか、または `custom` を選択してベースを入力します
17-
URLとアダプター。
16+
1. **プロバイダーを選択してください** — 71 個の組み込みレジストリプリセットのいずれか、または `custom` を選択してベース URL とアダプターを入力します。
1817
2. **API キー** — キーを貼り付けるか、`${ANTHROPIC_API_KEY}` のような環境変数を参照します。
1918
3. **デフォルト モデル** — キー、ローカル、カスタム プロバイダーの場合は、プリセットを受け入れるか、モデル ID を入力します。
2019
4. **プロキシ ポート** — デフォルトは `10100` です。

src/providers/free-directory.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ const CONNECTABLE: Record<string, ConnectableOverride> = {
132132
hyperbolic: openAi("https://api.hyperbolic.xyz/v1", "https://app.hyperbolic.xyz/settings", { verification: "official" }),
133133
longcat: openAi("https://api.longcat.chat/openai/v1", "https://longcat.chat", { verification: "official", discovery: "static", liveModels: false, models: ["LongCat-2.0"] }),
134134
monsterapi: openAi("https://api.monsterapi.ai/v1", "https://monsterapi.ai", { verification: "official" }),
135-
nebius: openAi("https://api.tokenfactory.nebius.com/v1", "https://tokenfactory.nebius.com", { supportLevel: "supported", verification: "official", documentationUrl: "https://docs.tokenfactory.nebius.com/quickstart", lastVerified: "2026-08-01" }),
135+
nebius: openAi("https://api.tokenfactory.nebius.com/v1", "https://tokenfactory.nebius.com", { supportLevel: "supported", verification: "official", documentationUrl: "https://docs.tokenfactory.nebius.com/quickstart", modelsUrl: "https://api.tokenfactory.nebius.com/v1/models?verbose=true", lastVerified: "2026-08-01" }),
136136
novita: openAi("https://api.novita.ai/openai/v1", "https://novita.ai/settings/key-management", { supportLevel: "supported", verification: "official", modelsUrl: "https://api.novita.ai/openai/v1/models" }),
137137
nscale: openAi("https://inference.api.nscale.com/v1", "https://console.nscale.com", { verification: "official" }),
138138
nvidia: openAi("https://integrate.api.nvidia.com/v1", "https://build.nvidia.com", { supportLevel: "supported", verification: "official", documentationUrl: "https://docs.api.nvidia.com/nim/reference/llm-apis" }),

src/providers/model-discovery.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { OcxProviderConfig } from "../types";
22
import {
33
getProviderRegistryEntry,
44
providerMatchesRegistryTransport,
5+
registryEntryForProviderDestination,
56
type ProviderModelDiscoveryFilter,
67
type ProviderModelDiscoveryPredicate,
78
type ProviderModelDiscoveryScalar,
@@ -124,9 +125,13 @@ export function resolveProviderModelDiscovery(
124125
providerName: string,
125126
provider: Pick<OcxProviderConfig, "baseUrl" | "adapter"> & Partial<Pick<OcxProviderConfig, "authMode">>,
126127
): ResolvedProviderModelDiscovery {
127-
const entry = providerMatchesRegistryTransport(providerName, provider)
128+
// The dashboard permits a canonical preset to be saved under a different name. Recover its
129+
// registry-owned discovery policy by transport in that case. The destination helper is limited
130+
// to exact fixed-key baseUrl + adapter matches, so custom endpoints, OAuth rows, templates, and
131+
// overridable destinations cannot acquire another provider's discovery URL or filter.
132+
const entry = (providerMatchesRegistryTransport(providerName, provider)
128133
? getProviderRegistryEntry(providerName)
129-
: undefined;
134+
: undefined) ?? registryEntryForProviderDestination(provider);
130135
const spec = entry?.modelDiscovery;
131136
return {
132137
...(spec ? { spec } : {}),

tests/provider-model-discovery-contract.test.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ import {
1313
readBoundedDiscoveryJson,
1414
resolveProviderModelDiscovery,
1515
} from "../src/providers/model-discovery";
16-
import { PROVIDER_REGISTRY, type ProviderModelDiscoverySpec } from "../src/providers/registry";
16+
import {
17+
PROVIDER_REGISTRY,
18+
registryEntryForProviderDestination,
19+
type ProviderModelDiscoverySpec,
20+
} from "../src/providers/registry";
1721
import { routeModel } from "../src/router";
1822
import type { OcxConfig, OcxProviderConfig } from "../src/types";
1923
import { withStubbedProviderFetch } from "./helpers/catalog-provider-fetch";
@@ -104,6 +108,18 @@ describe("registry-owned provider model discovery", () => {
104108
}
105109
});
106110

111+
test("keeps discovery-bearing fixed key destinations unambiguous for renamed presets", () => {
112+
for (const entry of PROVIDER_REGISTRY) {
113+
if (!entry.modelDiscovery || entry.authKind !== "key") continue;
114+
if (entry.allowBaseUrlOverride || /\{[^}]*\}/.test(entry.baseUrl)) continue;
115+
expect(registryEntryForProviderDestination({
116+
adapter: entry.adapter,
117+
baseUrl: entry.baseUrl,
118+
authMode: "key",
119+
})?.id).toBe(entry.id);
120+
}
121+
});
122+
107123
test("derives an alternate path and query only for the canonical destination", async () => {
108124
await withTogetherDiscovery({
109125
path: "catalog",
@@ -112,6 +128,14 @@ describe("registry-owned provider model discovery", () => {
112128
const canonical = buildModelsRequest(togetherConfig().providers.together!, "secret", "together");
113129
expect(canonical.url).toBe("https://api.together.xyz/v1/catalog?capability=chat&limit=100");
114130

131+
const renamedCanonical = buildModelsRequest(
132+
togetherConfig().providers.together!,
133+
"secret",
134+
"together-team",
135+
);
136+
expect(renamedCanonical.url)
137+
.toBe("https://api.together.xyz/v1/catalog?capability=chat&limit=100");
138+
115139
const collidingCustom: OcxProviderConfig = {
116140
adapter: "openai-chat",
117141
baseUrl: "https://custom.example/v9",

tests/sambanova-nebius-provider.test.ts

Lines changed: 70 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
deriveProviderPresets,
1313
providerConfigSeed,
1414
} from "../src/providers/derive";
15+
import { FREE_PROVIDER_DIRECTORY } from "../src/providers/free-directory";
1516
import { PROVIDER_REGISTRY, type ProviderRegistryEntry } from "../src/providers/registry";
1617
import { routedSlug } from "../src/providers/slug-codec";
1718
import { routeModel } from "../src/router";
@@ -51,6 +52,7 @@ afterEach(() => {
5152
globalThis.fetch = originalFetch;
5253
clearModelCache("sambanova");
5354
clearModelCache("nebius");
55+
clearModelCache("nebius-team");
5456
});
5557

5658
function registryEntry(id: ProviderId): ProviderRegistryEntry {
@@ -90,7 +92,9 @@ describe("SambaNova and Nebius providers", () => {
9092
dashboardUrl: PROVIDERS.sambanova.dashboardUrl,
9193
liveModels: true,
9294
preserveCustomDestination: true,
95+
apiKeyValidation: "unknown",
9396
parallelToolCalls: false,
97+
reasoningEfforts: [],
9498
modelDiscovery: {
9599
path: "models",
96100
maxResponseBytes: 131_072,
@@ -109,6 +113,7 @@ describe("SambaNova and Nebius providers", () => {
109113
liveModels: true,
110114
preserveCustomDestination: true,
111115
parallelToolCalls: false,
116+
reasoningEfforts: [],
112117
modelDiscovery: {
113118
path: "models",
114119
query: { verbose: "true" },
@@ -120,6 +125,9 @@ describe("SambaNova and Nebius providers", () => {
120125
},
121126
});
122127
expect(registryEntry("nebius").note).toContain("embedding and image-generation rows");
128+
expect(FREE_PROVIDER_DIRECTORY.find(row => row.id === "nebius")).toMatchObject({
129+
modelsUrl: PROVIDERS.nebius.modelsUrl,
130+
});
123131
});
124132

125133
test("derives CLI and dashboard presets without persisting registry trust policy", () => {
@@ -133,6 +141,7 @@ describe("SambaNova and Nebius providers", () => {
133141
baseUrl: provider.baseUrl,
134142
dashboardUrl: provider.dashboardUrl,
135143
liveModels: true,
144+
...(id === "sambanova" ? { apiKeyValidation: "unknown" } : {}),
136145
});
137146
expect(buildInitProviders().find(row => row.id === id)).toMatchObject({
138147
kind: "key",
@@ -151,34 +160,52 @@ describe("SambaNova and Nebius providers", () => {
151160
authMode: "key",
152161
liveModels: true,
153162
parallelToolCalls: false,
163+
reasoningEfforts: [],
154164
});
165+
expect(seed).not.toHaveProperty("apiKeyValidation");
155166
expect(seed).not.toHaveProperty("modelDiscovery");
156167
expect(seed).not.toHaveProperty("preserveCustomDestination");
157168
expect(KEY_LOGIN_PROVIDERS[id]).not.toHaveProperty("modelDiscovery");
158169
expect(KEY_LOGIN_PROVIDERS[id]).not.toHaveProperty("preserveCustomDestination");
159170
}
160171
});
161172

162-
test("lists and validates models through each registry-owned endpoint", async () => {
173+
test("builds each registry-owned models request and validates the authenticated Nebius catalog", async () => {
163174
for (const id of ["sambanova", "nebius"] as const) {
164175
const provider = PROVIDERS[id];
165176
expect(buildModelsRequest(providerConfig(id).providers[id]!, provider.key, id)).toEqual({
166177
url: provider.modelsUrl,
167178
headers: { Authorization: `Bearer ${provider.key}` },
168179
});
180+
}
169181

170-
globalThis.fetch = (async (input, init) => {
171-
expect(String(input)).toBe(provider.modelsUrl);
172-
expect(new Headers(init?.headers).get("authorization")).toBe(`Bearer ${provider.key}`);
173-
expect(init?.redirect).toBe("error");
174-
return new Response(provider.fixture, {
175-
status: 200,
176-
headers: { "content-type": "application/json" },
177-
});
178-
}) as typeof fetch;
182+
const provider = PROVIDERS.nebius;
183+
globalThis.fetch = (async (input, init) => {
184+
expect(String(input)).toBe(provider.modelsUrl);
185+
expect(new Headers(init?.headers).get("authorization")).toBe(`Bearer ${provider.key}`);
186+
expect(init?.redirect).toBe("error");
187+
return new Response(provider.fixture, {
188+
status: 200,
189+
headers: { "content-type": "application/json" },
190+
});
191+
}) as typeof fetch;
179192

180-
expect(await validateApiKey(id, KEY_LOGIN_PROVIDERS[id]!, provider.key)).toBe(true);
181-
}
193+
expect(await validateApiKey("nebius", KEY_LOGIN_PROVIDERS.nebius!, provider.key)).toBe(true);
194+
});
195+
196+
test("does not treat SambaNova's public model catalog as proof that a key is valid", async () => {
197+
let fetchCalled = false;
198+
globalThis.fetch = (async () => {
199+
fetchCalled = true;
200+
return new Response(SAMBANOVA_FIXTURE, { status: 200 });
201+
}) as typeof fetch;
202+
203+
expect(await validateApiKey(
204+
"sambanova",
205+
KEY_LOGIN_PROVIDERS.sambanova!,
206+
PROVIDERS.sambanova.key,
207+
)).toBe("unknown");
208+
expect(fetchCalled).toBe(false);
182209
});
183210

184211
test("filters Nebius mixed rows and preserves model metadata and native ids", async () => {
@@ -220,6 +247,7 @@ describe("SambaNova and Nebius providers", () => {
220247
expect(sambanovaModels[1]).toMatchObject({
221248
owned_by: "sambanova",
222249
contextWindow: 131_072,
250+
reasoningEfforts: [],
223251
});
224252
expect(nebiusModels.map(row => row.id)).toEqual([
225253
"meta-llama/Meta-Llama-3.1-8B-Instruct-fast",
@@ -230,6 +258,7 @@ describe("SambaNova and Nebius providers", () => {
230258
contextWindow: 131_072,
231259
inputModalities: ["text"],
232260
capabilities: ["function-calling", "json-mode"],
261+
reasoningEfforts: [],
233262
});
234263
expect(nebiusModels[1]).toMatchObject({
235264
contextWindow: 262_144,
@@ -246,6 +275,33 @@ describe("SambaNova and Nebius providers", () => {
246275
}
247276
});
248277

278+
test("keeps Nebius query and text-output filtering when the preset is renamed", async () => {
279+
const provider = PROVIDERS.nebius;
280+
globalThis.fetch = (async (input, init) => {
281+
expect(String(input)).toBe(provider.modelsUrl);
282+
expect(new Headers(init?.headers).get("authorization")).toBe(`Bearer ${provider.key}`);
283+
expect(init?.redirect).toBe("manual");
284+
return new Response(provider.fixture, {
285+
status: 200,
286+
headers: { "content-type": "application/json" },
287+
});
288+
}) as typeof fetch;
289+
290+
const renamed = "nebius-team";
291+
const config = withStubbedProviderFetch({
292+
port: 10100,
293+
defaultProvider: renamed,
294+
providers: {
295+
[renamed]: providerConfig("nebius").providers.nebius!,
296+
},
297+
} satisfies OcxConfig);
298+
const models = await gatherRoutedModels(config);
299+
expect(models.filter(row => row.provider === renamed).map(row => row.id)).toEqual([
300+
"meta-llama/Meta-Llama-3.1-8B-Instruct-fast",
301+
"Qwen/Qwen3-VL-235B-A22B-Instruct",
302+
]);
303+
});
304+
249305
test("routes tool requests to the fixed hosts without claiming parallel tool calls", () => {
250306
const cases = [
251307
["sambanova", "Meta-Llama-3.3-70B-Instruct"],
@@ -265,14 +321,15 @@ describe("SambaNova and Nebius providers", () => {
265321
}],
266322
},
267323
stream: true,
268-
options: {},
324+
options: { reasoning: "high" },
269325
});
270326
const body = JSON.parse(String(request.body)) as Record<string, unknown>;
271327

272328
expect(request.url).toBe(`${PROVIDERS[providerId].baseUrl}/chat/completions`);
273329
expect(request.headers.Authorization).toBe(`Bearer ${PROVIDERS[providerId].key}`);
274330
expect(body.model).toBe(modelId);
275331
expect(body.parallel_tool_calls).toBe(false);
332+
expect(body).not.toHaveProperty("reasoning_effort");
276333
}
277334
});
278335

0 commit comments

Comments
 (0)