fix(Upstage):Update Model list (#1855)

Integrated into release/v3.7.8
This commit is contained in:
backryun
2026-05-02 01:48:13 +09:00
committed by GitHub
parent a906bda7fc
commit fa950a8a49
7 changed files with 144 additions and 32 deletions

View File

@@ -66,6 +66,17 @@ export const EMBEDDING_PROVIDERS: Record<string, EmbeddingProvider> = {
],
},
upstage: {
id: "upstage",
baseUrl: "https://api.upstage.ai/v1/embeddings",
authType: "apikey",
authHeader: "bearer",
models: [
{ id: "embedding-query", name: "Embedding Query", dimensions: 4096 },
{ id: "embedding-passage", name: "Embedding Passage", dimensions: 4096 },
],
},
mistral: {
id: "mistral",
baseUrl: "https://api.mistral.ai/v1/embeddings",

View File

@@ -226,7 +226,7 @@ const CHAT_OPENAI_COMPAT_MODELS: Record<string, RegistryModel[]> = {
gigachat: buildModels(["GigaChat-2-Max", "GigaChat-2-Pro", "GigaChat-2-Lite"]),
venice: buildModels(["venice-latest"]),
codestral: buildModels(["codestral-2405", "codestral-latest"]),
upstage: buildModels(["solar-pro", "solar-mini", "solar-docvision", "solar-embedding-1-large"]),
upstage: buildModels(["solar-pro3", "solar-mini"]),
maritalk: buildModels(["sabia-3", "sabia-3-small"]),
"xiaomi-mimo": buildModels(["mimo-v2.5-pro", "mimo-v2.5", "mimo-v2-omni", "mimo-v2-flash"]),
"inference-net": buildModels([

View File

@@ -14,6 +14,7 @@ import {
} from "@/lib/localDb";
import {
SAFE_OUTBOUND_FETCH_PRESETS,
SafeOutboundFetchError,
getSafeOutboundFetchErrorStatus,
safeOutboundFetch,
} from "@/shared/network/safeOutboundFetch";
@@ -64,6 +65,7 @@ import {
} from "@/lib/providerModels/modelDiscovery";
type JsonRecord = Record<string, unknown>;
type LocalCatalogModel = { id: string; name?: string };
const antigravityDiscoveryInflight = new Map<
string,
@@ -118,6 +120,19 @@ function isNamedOpenAIStyleProvider(provider: string): boolean {
return NAMED_OPENAI_STYLE_PROVIDERS.has(provider);
}
function mergeLocalCatalogModels<T extends LocalCatalogModel, U extends LocalCatalogModel>(
registryCatalogModels: T[],
specialtyCatalogModels: U[]
): Array<T | U> {
if (registryCatalogModels.length === 0) return specialtyCatalogModels;
const registryModelIds = new Set(registryCatalogModels.map((model) => model.id));
return [
...registryCatalogModels,
...specialtyCatalogModels.filter((model) => !registryModelIds.has(model.id)),
];
}
function buildOptionalBearerHeaders(token: string | null | undefined): Record<string, string> {
return {
"Content-Type": "application/json",
@@ -402,55 +417,48 @@ export function getStaticModelsForProvider(
return staticModelsFn();
}
const specialtyModels: Array<{ id: string; name: string }> = [];
const appendModels = (models: Array<{ id: string; name?: string }>) => {
for (const model of models) {
if (specialtyModels.some((existing) => existing.id === model.id)) continue;
specialtyModels.push({
id: model.id,
name: model.name || model.id,
});
}
};
const embeddingProvider = getEmbeddingProvider(provider);
if (embeddingProvider) {
return embeddingProvider.models.map((model) => ({
id: model.id,
name: model.name || model.id,
}));
appendModels(embeddingProvider.models);
}
const rerankProvider = getRerankProvider(provider);
if (rerankProvider) {
return rerankProvider.models.map((model) => ({
id: model.id,
name: model.name || model.id,
}));
appendModels(rerankProvider.models);
}
const imageProvider = getImageProvider(provider);
if (imageProvider) {
return imageProvider.models.map((model) => ({
id: model.id,
name: model.name || model.id,
}));
appendModels(imageProvider.models);
}
const videoProvider = getVideoProvider(provider);
if (videoProvider) {
return videoProvider.models.map((model) => ({
id: model.id,
name: model.name || model.id,
}));
appendModels(videoProvider.models);
}
const speechProvider = getSpeechProvider(provider);
if (speechProvider) {
return speechProvider.models.map((model) => ({
id: model.id,
name: model.name || model.id,
}));
appendModels(speechProvider.models);
}
const transcriptionProvider = getTranscriptionProvider(provider);
if (transcriptionProvider) {
return transcriptionProvider.models.map((model) => ({
id: model.id,
name: model.name || model.id,
}));
appendModels(transcriptionProvider.models);
}
return undefined;
return specialtyModels.length > 0 ? specialtyModels : undefined;
}
// Provider models endpoints configuration
@@ -825,9 +833,8 @@ export async function GET(
const specialtyCatalogModels = getStaticModelsForProvider(provider) || [];
const toLocalCatalogModels = () => {
const localCatalog =
registryCatalogModels.length > 0 ? registryCatalogModels : specialtyCatalogModels;
return localCatalog.map((model: any) => ({
const localCatalog = mergeLocalCatalogModels(registryCatalogModels, specialtyCatalogModels);
return localCatalog.map((model) => ({
id: model.id,
name: model.name || model.id,
...(registryCatalogModels.length > 0 ? { owned_by: provider } : {}),
@@ -1755,13 +1762,12 @@ export async function GET(
});
}
const localCatalog =
registryCatalogModels.length > 0 ? registryCatalogModels : specialtyCatalogModels;
const localCatalog = mergeLocalCatalogModels(registryCatalogModels, specialtyCatalogModels);
if (!config && localCatalog.length > 0) {
return buildResponse({
provider,
connectionId,
models: localCatalog.map((m: any) => ({
models: localCatalog.map((m) => ({
id: m.id,
name: m.name || m.id,
...(registryCatalogModels.length > 0 ? { owned_by: provider } : {}),
@@ -1895,6 +1901,10 @@ export async function GET(
return buildApiDiscoveryResponse(allModels);
} catch (error) {
if (error instanceof SafeOutboundFetchError && error.code === "URL_GUARD_BLOCKED") {
return NextResponse.json({ error: error.message }, { status: 400 });
}
const status = getSafeOutboundFetchErrorStatus(error);
if (status) {
const message = error instanceof Error ? error.message : "Failed to fetch models";

View File

@@ -51,3 +51,13 @@ test("chat-openai-compat providers are registered across provider metadata, regi
assert.ok(models.length > 0, `${providerId} models must not be empty`);
}
});
test("upstage chat catalog does not include non-chat specialty models", () => {
const modelIds = REGISTRY.upstage.models.map((model) => model.id);
assert.ok(modelIds.includes("solar-pro3"));
assert.ok(modelIds.includes("solar-mini"));
assert.equal(modelIds.includes("document-parse"), false);
assert.equal(modelIds.includes("embedding-query"), false);
assert.equal(modelIds.includes("embedding-passage"), false);
});

View File

@@ -54,3 +54,22 @@ test("voyage-ai and jina-ai rerank registries expose supported models", () => {
assert.ok(all.some((model) => model.id === "voyage-ai/rerank-2.5"));
assert.ok(all.some((model) => model.id === "jina-ai/jina-reranker-v3"));
});
test("upstage embedding registry exposes current embedding models", () => {
const provider = getEmbeddingProvider("upstage");
assert.ok(provider);
assert.equal(provider.baseUrl, "https://api.upstage.ai/v1/embeddings");
assert.ok(provider.models.some((model) => model.id === "embedding-query"));
assert.ok(provider.models.some((model) => model.id === "embedding-passage"));
const parsed = parseEmbeddingModel("upstage/embedding-query");
assert.equal(parsed.provider, "upstage");
assert.equal(parsed.model, "embedding-query");
const all = getAllEmbeddingModels().filter((model) => model.provider === "upstage");
assert.deepEqual(
all.map((model) => model.id),
["upstage/embedding-query", "upstage/embedding-passage"]
);
});

View File

@@ -113,6 +113,49 @@ test("handleEmbedding supports resolved local providers without auth and preserv
}
});
test("handleEmbedding routes Upstage embedding models through the embedding endpoint", async () => {
const originalFetch = globalThis.fetch;
let captured;
globalThis.fetch = async (url, options = {}) => {
captured = {
url: String(url),
headers: options.headers,
body: JSON.parse(String(options.body || "{}")),
};
return new Response(
JSON.stringify({
data: [{ object: "embedding", embedding: [0.1, 0.2], index: 0 }],
usage: { prompt_tokens: 2, total_tokens: 2 },
}),
{ status: 200, headers: { "content-type": "application/json" } }
);
};
try {
const result = await handleEmbedding({
body: {
model: "upstage/embedding-query",
input: "Solar embeddings are useful",
},
credentials: { apiKey: "upstage-key" },
log: null,
});
assert.equal(result.success, true);
assert.equal(captured.url, "https://api.upstage.ai/v1/embeddings");
assert.equal(captured.headers.Authorization, "Bearer upstage-key");
assert.deepEqual(captured.body, {
model: "embedding-query",
input: "Solar embeddings are useful",
});
assert.equal(result.data.model, "upstage/embedding-query");
} finally {
globalThis.fetch = originalFetch;
}
});
test("handleEmbedding rejects invalid model strings without provider prefix", async () => {
const result = await handleEmbedding({
body: { model: "not-a-known-embedding-model", input: "hello" },

View File

@@ -435,6 +435,25 @@ test("provider models route returns the local catalog for new built-in chat-open
assert.ok(body.models.some((model) => model.id === "Qwen/Qwen3-Coder-480B-A35B-Instruct"));
});
test("provider models route merges Upstage chat and embedding catalogs", async () => {
const connection = await seedConnection("upstage", {
apiKey: "upstage-key",
});
const response = await callRoute(connection.id);
const body = (await response.json()) as any;
const modelIds = body.models.map((model) => model.id);
assert.equal(response.status, 200);
assert.equal(body.provider, "upstage");
assert.equal(body.source, "local_catalog");
assert.ok(modelIds.includes("solar-pro3"));
assert.ok(modelIds.includes("solar-mini"));
assert.ok(modelIds.includes("embedding-query"));
assert.ok(modelIds.includes("embedding-passage"));
assert.equal(modelIds.includes("document-parse"), false);
});
test("provider models route caches discovered opencode-go models per connection", async () => {
const connection = await seedConnection("opencode-go", {
apiKey: "opencode-go-key",