mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-10-07 12:58:49 +08:00
fix: route LLM metadata fetches through cloud proxy for cloud backends (#1109)
* fix: route LLM metadata fetches through cloud proxy for cloud backends When the active backend is 'cloud', ConfigService.searchProviders, ConfigService.searchModels, and fetchVerifiedModelsByProvider all called getAgentServerClientOptions(), which delegates to getEffectiveLocalBackend(). That returns null for cloud backends, so NoBackendAvailableError was thrown, surfacing as the 'No backend is configured' toast on /settings/llm. Follow the established cloud-vs-local branching pattern: for cloud backends, route the three LLM metadata endpoints (/api/llm/providers, /api/llm/models, /api/llm/models/verified) through callCloudProxy so correct Bearer auth is used and CORS is handled; for local backends keep the existing LLMMetadataClient path unchanged. Co-authored-by: openhands <openhands@all-hands.dev> * fix: extract named fields from raw cloud proxy LLM responses callCloudProxy returns the full JSON response body, while LLMMetadataClient extracts named sub-fields: getProviders() → response.data.providers (raw: { providers: string[] }) getModels() → response.data.models (raw: { models: string[] }) getVerifiedModels() → response.data.models (raw: { models: Record<string, string[]> }) The cloud proxy calls were treating the whole response object as the array/record directly, causing '(models ?? []).filter is not a function' and garbled provider/model dropdowns. Use typed raw-response wrappers and chain .then(raw => raw?.field ?? null) to mirror exactly what LLMMetadataClient does internally. Co-authored-by: openhands <openhands@all-hands.dev> * fix: use cloud API search endpoints for providers and models For cloud backends, /api/llm/providers and /api/llm/models are wrong — those are local agent-server endpoints. The cloud API exposes: GET /api/v1/config/providers/search → ProviderPage GET /api/v1/config/models/search → LLMModelPage Both return the same shape the local ConfigService types already define, so the cloud path calls callCloudProxy directly and returns the response as-is, with no reconstruction logic needed. The intermediate fetchVerifiedModelsByProvider step is skipped for cloud entirely — the cloud search endpoints embed verified status natively. Co-authored-by: openhands <openhands@all-hands.dev> * style: fix prettier formatting on buildCloudQueryString signature Co-authored-by: openhands <openhands@all-hands.dev> * docs: address review bot suggestions on cloud LLM metadata fix - Add JSDoc to ConfigService.searchModels and searchProviders noting that verifiedByProvider is ignored for cloud backends (cloud API embeds verified status directly on each returned item) - Expand comment on fetchVerifiedModelsByProvider's cloud early-return to clarify it is safe to treat as a no-op for cloud callers Co-authored-by: openhands <openhands@all-hands.dev> --------- Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
committed by
GitHub
co-authored by
openhands
parent
e096779809
commit
17db6dac05
@@ -1,5 +1,7 @@
|
||||
import { LLMMetadataClient } from "@openhands/typescript-client/clients";
|
||||
import { getAgentServerClientOptions } from "../agent-server-client-options";
|
||||
import { getActiveBackend } from "../backend-registry/active-store";
|
||||
import { callCloudProxy } from "../cloud/proxy";
|
||||
import type {
|
||||
LLMModel,
|
||||
LLMModelPage,
|
||||
@@ -42,11 +44,47 @@ function limitItems<T>(items: T[], limit?: number): T[] {
|
||||
return items.slice(0, limit);
|
||||
}
|
||||
|
||||
function buildCloudQueryString(
|
||||
params: Record<string, string | number | boolean | undefined>,
|
||||
): string {
|
||||
const qs = new URLSearchParams();
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (value !== undefined) qs.set(key, String(value));
|
||||
}
|
||||
const str = qs.toString();
|
||||
return str ? `?${str}` : "";
|
||||
}
|
||||
|
||||
class ConfigService {
|
||||
/**
|
||||
* @param verifiedByProvider - Pre-fetched verified-models map used by the
|
||||
* local reconstruction path. Ignored for cloud backends, which call
|
||||
* `/api/v1/config/models/search` directly (verified status is embedded in
|
||||
* each returned item).
|
||||
*/
|
||||
static async searchModels(
|
||||
params: SearchModelsParams = {},
|
||||
verifiedByProvider?: Record<string, string[]>,
|
||||
): Promise<LLMModelPage> {
|
||||
const active = getActiveBackend();
|
||||
|
||||
if (active.backend.kind === "cloud") {
|
||||
// Cloud exposes /api/v1/config/models/search which returns LLMModelPage directly.
|
||||
// verifiedByProvider is not needed — the cloud API embeds verified status natively.
|
||||
const qs = buildCloudQueryString({
|
||||
page_id: params.page_id,
|
||||
limit: params.limit,
|
||||
query: params.query,
|
||||
verified__eq: params.verified__eq,
|
||||
provider__eq: params.provider__eq,
|
||||
});
|
||||
return callCloudProxy<LLMModelPage>({
|
||||
backend: active.backend,
|
||||
method: "GET",
|
||||
path: `/api/v1/config/models/search${qs}`,
|
||||
});
|
||||
}
|
||||
|
||||
const llmClient = new LLMMetadataClient(getAgentServerClientOptions());
|
||||
const verifiedFetch =
|
||||
verifiedByProvider !== undefined
|
||||
@@ -87,16 +125,37 @@ class ConfigService {
|
||||
params.limit,
|
||||
);
|
||||
|
||||
return {
|
||||
items,
|
||||
next_page_id: null,
|
||||
};
|
||||
return { items, next_page_id: null };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param verifiedByProvider - Pre-fetched verified-models map used by the
|
||||
* local reconstruction path. Ignored for cloud backends, which call
|
||||
* `/api/v1/config/providers/search` directly (verified status is embedded in
|
||||
* each returned item).
|
||||
*/
|
||||
static async searchProviders(
|
||||
params: SearchProvidersParams = {},
|
||||
verifiedByProvider?: Record<string, string[]>,
|
||||
): Promise<ProviderPage> {
|
||||
const active = getActiveBackend();
|
||||
|
||||
if (active.backend.kind === "cloud") {
|
||||
// Cloud exposes /api/v1/config/providers/search which returns ProviderPage directly.
|
||||
// verifiedByProvider is not needed — the cloud API embeds verified status natively.
|
||||
const qs = buildCloudQueryString({
|
||||
page_id: params.page_id,
|
||||
limit: params.limit,
|
||||
query: params.query,
|
||||
verified__eq: params.verified__eq,
|
||||
});
|
||||
return callCloudProxy<ProviderPage>({
|
||||
backend: active.backend,
|
||||
method: "GET",
|
||||
path: `/api/v1/config/providers/search${qs}`,
|
||||
});
|
||||
}
|
||||
|
||||
const llmClient = new LLMMetadataClient(getAgentServerClientOptions());
|
||||
const verifiedFetch =
|
||||
verifiedByProvider !== undefined
|
||||
@@ -122,10 +181,7 @@ class ConfigService {
|
||||
params.limit,
|
||||
);
|
||||
|
||||
return {
|
||||
items,
|
||||
next_page_id: null,
|
||||
};
|
||||
return { items, next_page_id: null };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { LLMMetadataClient } from "@openhands/typescript-client/clients";
|
||||
import { getAgentServerClientOptions } from "#/api/agent-server-client-options";
|
||||
import { getActiveBackend } from "#/api/backend-registry/active-store";
|
||||
|
||||
export const VERIFIED_MODELS_QUERY_KEY = ["config", "verified-models"] as const;
|
||||
export const VERIFIED_MODELS_STALE_TIME = 1000 * 60 * 5;
|
||||
@@ -8,6 +9,14 @@ export const VERIFIED_MODELS_GC_TIME = 1000 * 60 * 15;
|
||||
export async function fetchVerifiedModelsByProvider(): Promise<
|
||||
Record<string, string[]>
|
||||
> {
|
||||
const active = getActiveBackend();
|
||||
if (active.backend.kind === "cloud") {
|
||||
// Cloud backends use /api/v1/config/providers/search and /api/v1/config/models/search,
|
||||
// which return verified status directly on each item. The intermediate
|
||||
// verifiedByProvider map is only used by the local ConfigService reconstruction
|
||||
// logic, so callers can safely treat this empty object as a no-op for cloud.
|
||||
return {};
|
||||
}
|
||||
const client = new LLMMetadataClient(getAgentServerClientOptions());
|
||||
return (await client.getVerifiedModels()) ?? {};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user