Skip to content

Commit 0c41341

Browse files
committed
fix(providers): address SambaNova and Nebius review feedback
1 parent 40c3a3c commit 0c41341

9 files changed

Lines changed: 57 additions & 19 deletions

File tree

docs-site/src/content/docs/guides/providers.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ slash-containing native model ids, and caps live discovery at 256 KiB and 256 ra
260260
serverless text and vision-language chat only; Hyperbolic's separate image, audio, and GPU endpoints
261261
are out of scope. Create keys at [Hyperbolic](https://app.hyperbolic.ai).
262262

263-
**SambaNova discovery.** The preset reads SambaCloud's public `/v1/models` list from the fixed API
263+
**SambaNova Cloud discovery.** The preset reads SambaNova Cloud's public `/v1/models` list from the fixed API
264264
host, preserves provider-native ids, and caps discovery at 128 KiB and 128 raw rows. Chat requests
265265
use the configured Bearer key and disable parallel function calls, which SambaNova does not yet
266266
support. Private SambaStudio deployment endpoints are out of scope. Create keys in

docs-site/src/content/docs/ja/guides/providers.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ Volcengine Agent Plan は `openai-responses` アダプターでネイティブ R
183183
vision-language chat のみを対象とし、別系統の image、audio、GPU endpoint は対象外です。キーは
184184
[Hyperbolic](https://app.hyperbolic.ai) で作成します。
185185

186-
**SambaNova の discovery:** preset は固定 API ホスト上の SambaCloud の公開 `/v1/models` 一覧を読み、
186+
**SambaNova Cloud の discovery:** preset は固定 API ホスト上の SambaNova Cloud の公開 `/v1/models` 一覧を読み、
187187
プロバイダー固有の ID を保持し、discovery を 128 KiB と raw 128 行に制限します。chat リクエストは
188188
設定済み Bearer キーを使い、SambaNova がまだ対応していない並列 function call は無効にします。
189189
非公開の SambaStudio deployment endpoint は対象外です。キーは

docs-site/src/content/docs/ko/guides/providers.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ Bearer API 키를 사용합니다. registry가 소유하는 DeepInfra 모델 목
182182
대상으로 하며 별도 image, audio, GPU 엔드포인트는 범위에서 제외합니다. 키는
183183
[Hyperbolic](https://app.hyperbolic.ai)에서 생성합니다.
184184

185-
**SambaNova 검색:** 프리셋은 고정 API 호스트의 SambaCloud 공개 `/v1/models` 목록을 읽고, 프로바이더
185+
**SambaNova Cloud 검색:** 프리셋은 고정 API 호스트의 SambaNova Cloud 공개 `/v1/models` 목록을 읽고, 프로바이더
186186
네이티브 ID를 보존하며 discovery를 128 KiB와 raw 행 128개로 제한합니다. chat 요청은 설정된 Bearer
187187
키를 사용하고 SambaNova가 아직 지원하지 않는 병렬 function call은 비활성화합니다. 비공개 SambaStudio
188188
deployment 엔드포인트는 범위에서 제외합니다. 키는 [SambaCloud](https://cloud.sambanova.ai/apis)에서 생성합니다.

docs-site/src/content/docs/ru/guides/providers.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ Volcengine Agent Plan использует нативную конечную т
192192
строками. Он охватывает только serverless text и vision-language chat; отдельные image, audio и GPU
193193
endpoint в него не входят. Ключи создаются в [Hyperbolic](https://app.hyperbolic.ai).
194194

195-
**Discovery для SambaNova.** Пресет читает общедоступный список SambaCloud `/v1/models` на
195+
**Discovery для SambaNova Cloud.** Пресет читает общедоступный список SambaNova Cloud `/v1/models` на
196196
фиксированном API-хосте, сохраняет нативные id провайдера и ограничивает discovery размером 128 KiB
197197
и 128 исходными строками. Chat-запросы используют настроенный Bearer-ключ; параллельные вызовы
198198
функций отключены, поскольку SambaNova пока их не поддерживает. Частные endpoint развёртываний

docs-site/src/content/docs/zh-cn/guides/providers.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ OpenAI Chat Completions 提供商。registry 固定的 DeepInfra 模型列表 UR
171171
并将实时发现限制为 256 KiB 和 256 条原始记录。它仅覆盖 serverless text 与 vision-language chat;独立的
172172
image、audio 和 GPU 端点不在范围内。密钥可在 [Hyperbolic](https://app.hyperbolic.ai) 创建。
173173

174-
**SambaNova 发现:**该预设从固定 API 主机读取 SambaCloud 的公开 `/v1/models` 列表,保留提供商原生
174+
**SambaNova Cloud 发现:**该预设从固定 API 主机读取 SambaNova Cloud 的公开 `/v1/models` 列表,保留提供商原生
175175
模型 id,并将发现限制为 128 KiB 和 128 条原始记录。chat 请求使用已配置的 Bearer 密钥;由于
176176
SambaNova 尚不支持并行 function call,该能力会被禁用。私有 SambaStudio 部署端点不在范围内。
177177
密钥可在 [SambaCloud](https://cloud.sambanova.ai/apis) 创建。

src/oauth/key-providers.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ export async function validateApiKey(
5252
key: string,
5353
): Promise<boolean | "unknown"> {
5454
try {
55+
// A public model catalog cannot prove that the supplied key is valid. Returning unknown keeps
56+
// the best-effort login flow available without persisting a false-positive validation result.
57+
if (provider.apiKeyValidation === "unknown") return "unknown";
58+
5559
if (provider.adapter === "anthropic") {
5660
const base = provider.baseUrl.replace(/\/v1\/?$/, "");
5761
const res = await fetch(`${base}/v1/messages`, {

src/providers/derive.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export interface DerivedKeyLoginProvider {
66
baseUrl: string;
77
responsesPath?: string;
88
adapter: string;
9+
apiKeyValidation?: "unknown";
910
apiKeyTransport?: OcxProviderConfig["apiKeyTransport"];
1011
dashboardUrl: string;
1112
models?: string[];
@@ -159,6 +160,7 @@ export function deriveKeyLoginMap(): Record<string, DerivedKeyLoginProvider> {
159160
baseUrl: entry.baseUrl,
160161
...(entry.responsesPath ? { responsesPath: entry.responsesPath } : {}),
161162
adapter: entry.adapter,
163+
...(entry.apiKeyValidation !== undefined ? { apiKeyValidation: entry.apiKeyValidation } : {}),
162164
...(entry.apiKeyTransport !== undefined ? { apiKeyTransport: entry.apiKeyTransport } : {}),
163165
dashboardUrl: entry.dashboardUrl,
164166
...(entry.models ? { models: [...entry.models] } : {}),

src/providers/registry.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ export interface ProviderRegistryEntry {
110110
allowKeyAuthOverride?: boolean;
111111
allowPrivateNetworkByDefault?: boolean;
112112
keyOptional?: boolean;
113+
/** Registry-only key-login policy for public model catalogs that cannot authenticate a key. */
114+
apiKeyValidation?: "unknown";
113115
/**
114116
* Free-tier pricing (no paid subscription required). Distinct from `keyOptional`:
115117
* free tiers may still require an API key (e.g. NVIDIA NIM free credits).
@@ -1054,14 +1056,17 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
10541056
dashboardUrl: "https://cloud.sambanova.ai/apis",
10551057
liveModels: true,
10561058
preserveCustomDestination: true,
1059+
apiKeyValidation: "unknown",
10571060
// SambaNova documents this request field but does not yet support parallel function calls.
10581061
parallelToolCalls: false,
1062+
// The public catalog does not report a trustworthy per-model reasoning contract.
1063+
reasoningEfforts: [],
10591064
modelDiscovery: {
10601065
path: "models",
10611066
maxResponseBytes: 128 * 1024,
10621067
maxModels: 128,
10631068
},
1064-
note: "SambaCloud text-generation models only; private SambaStudio deployment endpoints are outside this preset.",
1069+
note: "SambaNova Cloud text-generation models only; private SambaStudio deployment endpoints are outside this preset.",
10651070
},
10661071
{
10671072
id: "nebius",
@@ -1074,6 +1079,8 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
10741079
preserveCustomDestination: true,
10751080
// The public tools guide documents single function selection, not parallel tool calls.
10761081
parallelToolCalls: false,
1082+
// Missing reasoning metadata must not promote a model to Codex's full fallback ladder.
1083+
reasoningEfforts: [],
10771084
modelDiscovery: {
10781085
path: "models",
10791086
query: { verbose: "true" },

tests/sambanova-nebius-provider.test.ts

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,9 @@ describe("SambaNova and Nebius providers", () => {
9090
dashboardUrl: PROVIDERS.sambanova.dashboardUrl,
9191
liveModels: true,
9292
preserveCustomDestination: true,
93+
apiKeyValidation: "unknown",
9394
parallelToolCalls: false,
95+
reasoningEfforts: [],
9496
modelDiscovery: {
9597
path: "models",
9698
maxResponseBytes: 131_072,
@@ -109,6 +111,7 @@ describe("SambaNova and Nebius providers", () => {
109111
liveModels: true,
110112
preserveCustomDestination: true,
111113
parallelToolCalls: false,
114+
reasoningEfforts: [],
112115
modelDiscovery: {
113116
path: "models",
114117
query: { verbose: "true" },
@@ -133,6 +136,7 @@ describe("SambaNova and Nebius providers", () => {
133136
baseUrl: provider.baseUrl,
134137
dashboardUrl: provider.dashboardUrl,
135138
liveModels: true,
139+
...(id === "sambanova" ? { apiKeyValidation: "unknown" } : {}),
136140
});
137141
expect(buildInitProviders().find(row => row.id === id)).toMatchObject({
138142
kind: "key",
@@ -151,34 +155,52 @@ describe("SambaNova and Nebius providers", () => {
151155
authMode: "key",
152156
liveModels: true,
153157
parallelToolCalls: false,
158+
reasoningEfforts: [],
154159
});
160+
expect(seed).not.toHaveProperty("apiKeyValidation");
155161
expect(seed).not.toHaveProperty("modelDiscovery");
156162
expect(seed).not.toHaveProperty("preserveCustomDestination");
157163
expect(KEY_LOGIN_PROVIDERS[id]).not.toHaveProperty("modelDiscovery");
158164
expect(KEY_LOGIN_PROVIDERS[id]).not.toHaveProperty("preserveCustomDestination");
159165
}
160166
});
161167

162-
test("lists and validates models through each registry-owned endpoint", async () => {
168+
test("builds each registry-owned models request and validates the authenticated Nebius catalog", async () => {
163169
for (const id of ["sambanova", "nebius"] as const) {
164170
const provider = PROVIDERS[id];
165171
expect(buildModelsRequest(providerConfig(id).providers[id]!, provider.key, id)).toEqual({
166172
url: provider.modelsUrl,
167173
headers: { Authorization: `Bearer ${provider.key}` },
168174
});
175+
}
169176

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

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

184206
test("filters Nebius mixed rows and preserves model metadata and native ids", async () => {
@@ -220,6 +242,7 @@ describe("SambaNova and Nebius providers", () => {
220242
expect(sambanovaModels[1]).toMatchObject({
221243
owned_by: "sambanova",
222244
contextWindow: 131_072,
245+
reasoningEfforts: [],
223246
});
224247
expect(nebiusModels.map(row => row.id)).toEqual([
225248
"meta-llama/Meta-Llama-3.1-8B-Instruct-fast",
@@ -230,6 +253,7 @@ describe("SambaNova and Nebius providers", () => {
230253
contextWindow: 131_072,
231254
inputModalities: ["text"],
232255
capabilities: ["function-calling", "json-mode"],
256+
reasoningEfforts: [],
233257
});
234258
expect(nebiusModels[1]).toMatchObject({
235259
contextWindow: 262_144,
@@ -265,14 +289,15 @@ describe("SambaNova and Nebius providers", () => {
265289
}],
266290
},
267291
stream: true,
268-
options: {},
292+
options: { reasoning: "high" },
269293
});
270294
const body = JSON.parse(String(request.body)) as Record<string, unknown>;
271295

272296
expect(request.url).toBe(`${PROVIDERS[providerId].baseUrl}/chat/completions`);
273297
expect(request.headers.Authorization).toBe(`Bearer ${PROVIDERS[providerId].key}`);
274298
expect(body.model).toBe(modelId);
275299
expect(body.parallel_tool_calls).toBe(false);
300+
expect(body).not.toHaveProperty("reasoning_effort");
276301
}
277302
});
278303

0 commit comments

Comments
 (0)