server/agent-endpoint-models.ts
Rama main · réplica de lectura del repositorio original.
import type { InstanceConfig } from "./contracts.ts";
import { CLAUDE_MODEL_ID } from "./drivers/claude-model-extras.ts";
import { STATIC_CODEX_MODELS } from "./drivers/codex-catalog.ts";
import { currentEngineCatalog } from "./model-manifest/manifest.ts";
import { isLoopback, providerModelsUrl } from "./provider-key-check.ts";
const OFFICIAL_URL = { codex: "https://api.openai.com/v1", claude: "https://api.anthropic.com" } as const;
const MAX_CATALOG_BYTES = 2 * 1024 * 1024;
const MAX_MODELS = 1000;
const TYPE_MODEL = " Or type the model the host serves in Model.";
export function agentEndpointAddress(entry: InstanceConfig): string {
return entry.driver === "codex"
? (entry.config as { endpoint: { url: string } }).endpoint.url
: entry.environment!.ANTHROPIC_BASE_URL;
}
export function isGatewayEndpoint(entry: InstanceConfig): boolean {
return agentEndpointAddress(entry) !== OFFICIAL_URL[entry.driver === "codex" ? "codex" : "claude"];
}
/** An endpoint saved with a fixed model keeps it; discovery never replaces it. */
export function hasExplicitModel(entry: InstanceConfig): boolean {
return entry.driver === "codex"
? Boolean((entry.config as { endpoint?: { model?: string } }).endpoint?.model)
: Boolean(entry.environment?.ANTHROPIC_MODEL);
}
export function officialModels(codex: boolean): string[] {
const catalog = codex ? STATIC_CODEX_MODELS : currentEngineCatalog("claude");
return [...new Set([catalog.default, ...catalog.options.map(option => option.id)])];
}
/** Reads at most `limit` bytes, so a gateway cannot make the server buffer an unbounded body. */
async function readCatalog(response: Response, limit: number): Promise<string | null> {
const reader = response.body?.getReader();
if (!reader) return "";
const chunks: Uint8Array[] = [];
let size = 0;
for (;;) {
const { done, value } = await reader.read();
if (done) break;
size += value.byteLength;
if (size > limit) { await reader.cancel().catch(() => {}); return null; }
chunks.push(value);
}
return Buffer.concat(chunks).toString("utf8");
}
/** Resolves the catalog for per-agent selection without storing a provider-wide model choice. */
export async function discoverAgentEndpointModels(entry: InstanceConfig, fetchImpl: typeof fetch = fetch): Promise<void> {
const codex = entry.driver === "codex";
const config = entry.config as { endpoint?: { url: string; models: string[] }; endpointModels?: string[] };
const setModels = (models: string[]) => { if (codex) config.endpoint!.models = models; else config.endpointModels = models; };
if (!isGatewayEndpoint(entry)) return setModels(officialModels(codex));
const env = entry.environment ?? {};
const token = codex ? env.OPENMAUSBOT_CODEX_API_KEY : env.ANTHROPIC_API_KEY;
const headers = codex ? { authorization: `Bearer ${token}` } : { authorization: `Bearer ${token}`, "x-api-key": token!, "anthropic-version": "2023-06-01" };
const modelsUrl = providerModelsUrl(codex ? "openai" : "anthropic", agentEndpointAddress(entry));
const failure = (detail: string) => Object.assign(new Error(`Could not discover gateway models: ${detail}`), { status: 400 });
// Same rule as checkProviderKey: the key goes out only over HTTPS, or over plain HTTP to this computer.
const target = new URL(modelsUrl);
if (target.protocol !== "https:" && !(target.protocol === "http:" && isLoopback(target.hostname))) {
throw failure(`OpenMausBot reads a model list only over HTTPS or from this computer, so your key is not sent in the clear.${TYPE_MODEL}`);
}
let response: Response;
try {
// A redirect must never replay the credential to another address.
response = await fetchImpl(modelsUrl, {
headers, redirect: "manual", signal: AbortSignal.timeout(10_000),
});
} catch (error) {
const timeout = error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError");
const localHint = isLoopback(new URL(modelsUrl).hostname)
? " localhost refers to OpenMausBot's server or container. If both apps run on this computer, try 127.0.0.1; otherwise use an address reachable from that server."
: " Check that the gateway address is reachable from OpenMausBot's server.";
throw failure((timeout ? "The request timed out." : "OpenMausBot's server could not reach the gateway.") + localHint);
}
if (!response.ok) await response.body?.cancel().catch(() => {});
if (response.status === 401 || response.status === 403) {
throw failure(`The gateway rejected the API key (HTTP ${response.status}). Use the gateway's client API key, not a ChatGPT or Claude login token.`);
}
if (response.status === 404) throw failure(`The model catalog was not found (HTTP 404). Check the base URL; Codex usually needs /v1 and requests /v1/models.${TYPE_MODEL}`);
if (response.status >= 300 && response.status < 400) throw failure("The gateway redirected the model request. Enter the final API base URL; credentials are never forwarded through redirects.");
if (!response.ok) throw failure(`The model request failed (HTTP ${response.status}). Check the gateway's status and model catalog support.`);
const text = await readCatalog(response, MAX_CATALOG_BYTES)
.catch(() => { throw failure("The gateway closed the connection while sending its model catalog. Try again, or check the gateway's logs."); });
if (text === null) throw failure("The model catalog is larger than 2 MB. Check that the base URL points to the gateway's API.");
let body: unknown;
try { body = JSON.parse(text); } catch { throw failure("The gateway did not return a JSON model catalog. Check that the base URL points to its API, not its dashboard."); }
const data = body && typeof body === "object" ? (body as { data?: unknown }).data : undefined;
const pattern = codex ? /^[\w][\w./+-]*$/ : CLAUDE_MODEL_ID;
const models = new Set<string>();
for (const item of Array.isArray(data) ? data : []) {
if (models.size >= MAX_MODELS) break;
const id = item && typeof item === "object" ? (item as { id?: unknown }).id : undefined;
// Claude Code reads `--model default` as its own default model, not a gateway model id.
if (typeof id === "string" && id.length <= 160 && pattern.test(id) && (codex || id !== "default")) models.add(id);
}
if (!models.size) throw failure(`The gateway returned no supported model IDs. Connect a provider account in the gateway and check its /models catalog.${TYPE_MODEL}`);
setModels([...models]);
}