Skip to content

Commit b454bba

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

5 files changed

Lines changed: 116 additions & 20 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: 9 additions & 3 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,14 @@ 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-
? getProviderRegistryEntry(providerName)
129-
: undefined;
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 namedEntry = getProviderRegistryEntry(providerName);
133+
const entry = namedEntry
134+
? (providerMatchesRegistryTransport(providerName, provider) ? namedEntry : undefined)
135+
: registryEntryForProviderDestination(provider);
130136
const spec = entry?.modelDiscovery;
131137
return {
132138
...(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: 80 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import {
1212
deriveProviderPresets,
1313
providerConfigSeed,
1414
} from "../src/providers/derive";
15+
import { FREE_PROVIDER_DIRECTORY } from "../src/providers/free-directory";
16+
import { resolveProviderModelDiscovery } from "../src/providers/model-discovery";
1517
import { PROVIDER_REGISTRY, type ProviderRegistryEntry } from "../src/providers/registry";
1618
import { routedSlug } from "../src/providers/slug-codec";
1719
import { routeModel } from "../src/router";
@@ -51,6 +53,7 @@ afterEach(() => {
5153
globalThis.fetch = originalFetch;
5254
clearModelCache("sambanova");
5355
clearModelCache("nebius");
56+
clearModelCache("nebius-team");
5457
});
5558

5659
function registryEntry(id: ProviderId): ProviderRegistryEntry {
@@ -90,7 +93,9 @@ describe("SambaNova and Nebius providers", () => {
9093
dashboardUrl: PROVIDERS.sambanova.dashboardUrl,
9194
liveModels: true,
9295
preserveCustomDestination: true,
96+
apiKeyValidation: "unknown",
9397
parallelToolCalls: false,
98+
reasoningEfforts: [],
9499
modelDiscovery: {
95100
path: "models",
96101
maxResponseBytes: 131_072,
@@ -109,6 +114,7 @@ describe("SambaNova and Nebius providers", () => {
109114
liveModels: true,
110115
preserveCustomDestination: true,
111116
parallelToolCalls: false,
117+
reasoningEfforts: [],
112118
modelDiscovery: {
113119
path: "models",
114120
query: { verbose: "true" },
@@ -120,6 +126,9 @@ describe("SambaNova and Nebius providers", () => {
120126
},
121127
});
122128
expect(registryEntry("nebius").note).toContain("embedding and image-generation rows");
129+
expect(FREE_PROVIDER_DIRECTORY.find(row => row.id === "nebius")).toMatchObject({
130+
modelsUrl: PROVIDERS.nebius.modelsUrl,
131+
});
123132
});
124133

125134
test("derives CLI and dashboard presets without persisting registry trust policy", () => {
@@ -133,6 +142,7 @@ describe("SambaNova and Nebius providers", () => {
133142
baseUrl: provider.baseUrl,
134143
dashboardUrl: provider.dashboardUrl,
135144
liveModels: true,
145+
...(id === "sambanova" ? { apiKeyValidation: "unknown" } : {}),
136146
});
137147
expect(buildInitProviders().find(row => row.id === id)).toMatchObject({
138148
kind: "key",
@@ -151,34 +161,52 @@ describe("SambaNova and Nebius providers", () => {
151161
authMode: "key",
152162
liveModels: true,
153163
parallelToolCalls: false,
164+
reasoningEfforts: [],
154165
});
166+
expect(seed).not.toHaveProperty("apiKeyValidation");
155167
expect(seed).not.toHaveProperty("modelDiscovery");
156168
expect(seed).not.toHaveProperty("preserveCustomDestination");
157169
expect(KEY_LOGIN_PROVIDERS[id]).not.toHaveProperty("modelDiscovery");
158170
expect(KEY_LOGIN_PROVIDERS[id]).not.toHaveProperty("preserveCustomDestination");
159171
}
160172
});
161173

162-
test("lists and validates models through each registry-owned endpoint", async () => {
174+
test("builds each registry-owned models request and validates the authenticated Nebius catalog", async () => {
163175
for (const id of ["sambanova", "nebius"] as const) {
164176
const provider = PROVIDERS[id];
165177
expect(buildModelsRequest(providerConfig(id).providers[id]!, provider.key, id)).toEqual({
166178
url: provider.modelsUrl,
167179
headers: { Authorization: `Bearer ${provider.key}` },
168180
});
181+
}
169182

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;
183+
const provider = PROVIDERS.nebius;
184+
globalThis.fetch = (async (input, init) => {
185+
expect(String(input)).toBe(provider.modelsUrl);
186+
expect(new Headers(init?.headers).get("authorization")).toBe(`Bearer ${provider.key}`);
187+
expect(init?.redirect).toBe("error");
188+
return new Response(provider.fixture, {
189+
status: 200,
190+
headers: { "content-type": "application/json" },
191+
});
192+
}) as typeof fetch;
179193

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

184212
test("filters Nebius mixed rows and preserves model metadata and native ids", async () => {
@@ -220,6 +248,7 @@ describe("SambaNova and Nebius providers", () => {
220248
expect(sambanovaModels[1]).toMatchObject({
221249
owned_by: "sambanova",
222250
contextWindow: 131_072,
251+
reasoningEfforts: [],
223252
});
224253
expect(nebiusModels.map(row => row.id)).toEqual([
225254
"meta-llama/Meta-Llama-3.1-8B-Instruct-fast",
@@ -230,6 +259,7 @@ describe("SambaNova and Nebius providers", () => {
230259
contextWindow: 131_072,
231260
inputModalities: ["text"],
232261
capabilities: ["function-calling", "json-mode"],
262+
reasoningEfforts: [],
233263
});
234264
expect(nebiusModels[1]).toMatchObject({
235265
contextWindow: 262_144,
@@ -246,6 +276,33 @@ describe("SambaNova and Nebius providers", () => {
246276
}
247277
});
248278

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

272329
expect(request.url).toBe(`${PROVIDERS[providerId].baseUrl}/chat/completions`);
273330
expect(request.headers.Authorization).toBe(`Bearer ${PROVIDERS[providerId].key}`);
274331
expect(body.model).toBe(modelId);
275332
expect(body.parallel_tool_calls).toBe(false);
333+
expect(body).not.toHaveProperty("reasoning_effort");
276334
}
277335
});
278336

@@ -300,5 +358,14 @@ describe("SambaNova and Nebius providers", () => {
300358
authMode: "key",
301359
});
302360
}
361+
362+
const crossPreset = providerConfig("sambanova", {
363+
baseUrl: PROVIDERS.nebius.baseUrl,
364+
}).providers.sambanova!;
365+
expect(resolveProviderModelDiscovery("sambanova", crossPreset).spec).toBeUndefined();
366+
expect(buildModelsRequest(crossPreset, "custom-key", "sambanova")).toEqual({
367+
url: `${PROVIDERS.nebius.baseUrl}/models`,
368+
headers: { Authorization: "Bearer custom-key" },
369+
});
303370
});
304371
});

0 commit comments

Comments
 (0)