Extract OpenAPI provider routing and payload seams

This commit is contained in:
OpenPets Dev 2026-06-19 21:15:17 +00:00
parent 8ff7ae1625
commit 9c6677564c
7 changed files with 369 additions and 343 deletions

View file

@ -145,6 +145,10 @@ const controlCenterIntegrationsMcpToolkitSectionSource = readFileSync(join(appDi
const openApiChatSource = readFileSync(join(appDir, "src", "openapi-chat.ts"), "utf8");
const openApiChatPromptFlowsSource = readFileSync(join(appDir, "src", "openapi-chat-prompt-flows.ts"), "utf8");
const openApiChatPromptPlainFlowSource = readFileSync(join(appDir, "src", "openapi-chat-prompt-plain-flow.ts"), "utf8");
const openApiChatProviderSource = readFileSync(join(appDir, "src", "openapi-chat-provider.ts"), "utf8");
const openApiChatProviderSupportSource = readFileSync(join(appDir, "src", "openapi-chat-provider-support.ts"), "utf8");
const openApiChatProviderRoutingSource = readFileSync(join(appDir, "src", "openapi-chat-provider-routing.ts"), "utf8");
const openApiChatProviderPayloadsSource = readFileSync(join(appDir, "src", "openapi-chat-provider-payloads.ts"), "utf8");
const openApiChatSettingsSource = readFileSync(join(appDir, "src", "openapi-chat-settings.ts"), "utf8");
const openApiChatModelSource = readFileSync(join(appDir, "src", "openapi-chat-model.ts"), "utf8");
const openApiChatPresentationSource = readFileSync(join(appDir, "src", "openapi-chat-presentation.ts"), "utf8");
@ -715,6 +719,14 @@ assert.match(openApiChatPromptFlowsSource, /export \{[\s\S]*?buildOpenApiInstruc
assert.match(openApiChatPromptFlowsSource, /from "\.\/openapi-chat-presentation(?:\.js)?"/, "OpenAPI chat prompt flows must import the extracted presentation seam.");
assert.match(openApiChatPromptFlowsSource, /from "\.\/openapi-chat-provider(?:\.js)?"/, "OpenAPI chat prompt flows must import the extracted provider seam.");
assert.match(openApiChatPromptFlowsSource, /from "\.\/openapi-chat-tool-loop(?:\.js)?"/, "OpenAPI chat prompt flows must import the extracted tool loop seam.");
assert.match(openApiChatProviderSource, /export \* from "\.\/openapi-chat-provider-support(?:\.js)?"/, "OpenAPI chat provider barrel must re-export the extracted provider support seam.");
assert.match(openApiChatProviderSupportSource, /export \* from "\.\/openapi-chat-provider-routing(?:\.js)?"/, "OpenAPI chat provider support seam must re-export routing helpers.");
assert.match(openApiChatProviderSupportSource, /export \* from "\.\/openapi-chat-provider-payloads(?:\.js)?"/, "OpenAPI chat provider support seam must re-export payload helpers.");
assert.match(openApiChatProviderRoutingSource, /export function buildRequestAttempts/, "OpenAPI chat provider routing seam must export request-attempt routing.");
assert.match(openApiChatProviderRoutingSource, /export async function readProviderError/, "OpenAPI chat provider routing seam must export provider error normalization.");
assert.match(openApiChatProviderPayloadsSource, /export function parseResponsesPayload/, "OpenAPI chat provider payload seam must export responses payload parsing.");
assert.match(openApiChatProviderPayloadsSource, /export function parseChatCompletionsPayload/, "OpenAPI chat provider payload seam must export chat-completions payload parsing.");
assert.match(openApiChatProviderPayloadsSource, /export function extractChatCompletionsChoice/, "OpenAPI chat provider payload seam must export tool-call extraction.");
assert.match(openApiChatPromptPlainFlowSource, /export async function sendOpenApiChatPromptPlainFlow/, "OpenAPI chat plain-flow seam must export the plain prompt runner.");
assert.match(openApiChatPromptPlainFlowSource, /export function buildOpenApiInstructions/, "OpenAPI chat plain-flow seam must export instruction building.");
assert.match(openApiChatPromptPlainFlowSource, /from "\.\/openapi-chat-presentation(?:\.js)?"/, "OpenAPI chat plain-flow seam must import the extracted presentation seam.");

View file

@ -242,7 +242,10 @@ plugin-service-local-support.ts → plugin-local-loader.ts validates selected fo
- `openapi-chat-request-helpers.ts`: Extracted pure request-body and chat-completions history shaping for prompt-window OpenAPI chat
- `openapi-chat-conversation-store.ts`: Extracted current-conversation state ownership and disk-backed persistence for prompt-window chat
- `openapi-chat-conversation-store-core.ts`: Pure transcript normalization, per-conversation capping, and title helpers for the extracted conversation store seam
- `openapi-chat-provider.ts`: Extracted provider transport helpers for endpoint routing, auth headers, provider error normalization, and response/tool-call payload parsing
- `openapi-chat-provider.ts`: Stable provider barrel for prompt-window OpenAPI chat transport helpers.
- `openapi-chat-provider-support.ts`: Public provider support barrel that keeps prompt-window imports stable while forwarding to focused routing and payload seams.
- `openapi-chat-provider-routing.ts`: Extracted endpoint routing, auth header shaping, provider labeling, and normalized provider-error fallback decisions.
- `openapi-chat-provider-payloads.ts`: Extracted responses/chat-completions payload parsing and tool-call envelope extraction for the MCP loop.
- `renderer/`: Vite React/Tailwind Control Center shell for Dashboard, Familiars, Integrations, Plugins, and Settings, with extracted integrations/settings/helper route seams under `renderer/src/control-center/`, including the `settings-view-state.ts` hook seam, the `settings-view-state-actions.ts` route action shell, and the extracted state plugin/knowledge/type seams that keep route mutations bounded.
**Familiars**:

View file

@ -0,0 +1,98 @@
type ChatCompletionToolCall = {
readonly id: string;
readonly type: "function";
readonly function: {
readonly name: string;
readonly arguments: string;
};
};
export interface ChatCompletionChoice {
readonly message: {
readonly content?: string | null;
readonly tool_calls?: readonly ChatCompletionToolCall[];
};
}
export function parseResponsesPayload(payload: Record<string, unknown>): { readonly text: string; readonly responseId?: string } {
const text = extractResponsesOutputText(payload).trim();
const responseId = typeof payload.id === "string" && payload.id ? payload.id : undefined;
return { text, responseId };
}
export function parseChatCompletionsPayload(payload: Record<string, unknown>): { readonly text: string; readonly responseId: string } {
const text = extractChatCompletionsText(payload).trim();
const responseId = typeof payload.id === "string" && payload.id
? payload.id
: `chatcmpl-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
return { text, responseId };
}
export function extractChatCompletionsChoice(payload: Record<string, unknown>): ChatCompletionChoice | undefined {
const choices = Array.isArray(payload.choices) ? payload.choices : [];
const first = choices[0];
if (!first || typeof first !== "object") return undefined;
const message = (first as Record<string, unknown>).message;
if (!message || typeof message !== "object") return undefined;
const msg = message as Record<string, unknown>;
const content = typeof msg.content === "string" ? msg.content : null;
const rawToolCalls = Array.isArray(msg.tool_calls) ? msg.tool_calls : [];
const tool_calls = rawToolCalls.map((tc): ChatCompletionToolCall | null => {
if (!tc || typeof tc !== "object") return null;
const t = tc as Record<string, unknown>;
const id = typeof t.id === "string" ? t.id : "";
const type: "function" = t.type === "function" ? "function" : "function";
const fn = t.function;
if (!fn || typeof fn !== "object") return null;
const f = fn as Record<string, unknown>;
const name = typeof f.name === "string" ? f.name : "";
const args = typeof f.arguments === "string" ? f.arguments : "";
if (!id || !name) return null;
return { id, type, function: { name, arguments: args } };
}).filter((tc): tc is ChatCompletionToolCall => tc !== null);
return { message: { content, ...(tool_calls.length > 0 ? { tool_calls } : {}) } };
}
function extractResponsesOutputText(payload: Record<string, unknown>): string {
if (typeof payload.output_text === "string") return payload.output_text;
const output = Array.isArray(payload.output) ? payload.output : [];
const parts: string[] = [];
for (const item of output) {
if (!isRecord(item) || item.type !== "message" || !Array.isArray(item.content)) continue;
for (const contentPart of item.content) {
if (!isRecord(contentPart) || contentPart.type !== "output_text" || typeof contentPart.text !== "string") continue;
parts.push(contentPart.text);
}
}
return parts.join("");
}
function extractChatCompletionsText(payload: Record<string, unknown>): string {
const choices = Array.isArray(payload.choices) ? payload.choices : [];
const parts: string[] = [];
for (const choice of choices) {
if (!isRecord(choice) || !isRecord(choice.message)) continue;
const content = choice.message.content;
if (typeof content === "string") {
parts.push(content);
continue;
}
if (!Array.isArray(content)) continue;
for (const part of content) {
if (!isRecord(part)) continue;
if (typeof part.text === "string") {
parts.push(part.text);
} else if (isRecord(part.text) && typeof part.text.value === "string") {
parts.push(part.text.value);
}
}
}
return parts.join("");
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}

View file

@ -0,0 +1,241 @@
export type OpenApiChatTransport = "responses" | "chat-completions";
export interface OpenApiRequestAttempt {
readonly transport: OpenApiChatTransport;
readonly endpoint: string;
}
interface ProviderErrorPayload {
readonly error?: {
readonly message?: unknown;
readonly code?: unknown;
readonly type?: unknown;
};
}
export interface ProviderErrorInfo {
readonly endpoint: string;
readonly status: number;
readonly providerMessage: string;
readonly providerCode: string;
readonly providerType: string;
readonly normalizedMessage: string;
}
export function buildRequestAttempts(endpoint: string): readonly OpenApiRequestAttempt[] {
const url = safeParseUrl(endpoint);
if (!url) {
return [{ transport: "responses", endpoint }];
}
const normalizedPath = url.pathname.replace(/\/+$/, "");
if (normalizedPath.endsWith("/chat/completions")) {
return [{ transport: "chat-completions", endpoint: url.toString() }];
}
if (normalizedPath.endsWith("/responses")) {
if (isOfficialOpenAiEndpoint(endpoint) || isAzureOpenAiEndpoint(endpoint)) {
return [{ transport: "responses", endpoint: url.toString() }];
}
return dedupeAttempts([
{ transport: "responses", endpoint: url.toString() },
{ transport: "chat-completions", endpoint: withPathname(url, replaceTransportPath(normalizedPath, "chat-completions")) },
]);
}
if (normalizedPath.endsWith("/v1")) {
return dedupeAttempts([
{ transport: "responses", endpoint: withPathname(url, `${normalizedPath}/responses`) },
{ transport: "chat-completions", endpoint: withPathname(url, `${normalizedPath}/chat/completions`) },
]);
}
return [{ transport: "responses", endpoint: url.toString() }];
}
export function buildOpenApiRequestHeaders(endpoint: string, credential: string): Record<string, string> {
const headers: Record<string, string> = {
"Content-Type": "application/json",
Authorization: `Bearer ${credential}`,
};
if (isAzureOpenAiEndpoint(endpoint)) {
headers["api-key"] = credential;
} else if (!isOfficialOpenAiEndpoint(endpoint)) {
headers["x-api-key"] = credential;
}
return headers;
}
export function getEndpointHost(endpoint: string): string {
return safeParseUrl(endpoint)?.host ?? "unknown";
}
export async function readProviderError(response: Response, endpoint: string): Promise<ProviderErrorInfo> {
let providerMessage = "";
let providerCode = "";
let providerType = "";
try {
const payload = await response.json() as ProviderErrorPayload;
providerMessage = typeof payload?.error?.message === "string" ? payload.error.message : "";
providerCode = typeof payload?.error?.code === "string" ? payload.error.code : "";
providerType = typeof payload?.error?.type === "string" ? payload.error.type : "";
} catch {
// Fall through to a generic message.
}
return {
endpoint,
status: response.status,
providerMessage,
providerCode,
providerType,
normalizedMessage: normalizeProviderErrorMessage({
endpoint,
status: response.status,
providerMessage,
providerCode,
providerType,
}),
};
}
export function shouldTryChatCompletionsFallback(attempt: OpenApiRequestAttempt, errorInfo: ProviderErrorInfo): boolean {
if (attempt.transport !== "responses") return false;
if (isOfficialOpenAiEndpoint(attempt.endpoint) || isAzureOpenAiEndpoint(attempt.endpoint)) return false;
const normalizedProviderMessage = sanitizeProviderMessage(errorInfo.providerMessage);
const haystack = `${errorInfo.providerCode} ${errorInfo.providerType} ${normalizedProviderMessage}`.toLowerCase();
if (errorInfo.status === 401 || errorInfo.status === 403 || errorInfo.status === 429) return false;
if (haystack.includes("authentication header") || haystack.includes("invalid api key") || haystack.includes("incorrect api key")) return false;
if (haystack.includes("quota") || haystack.includes("billing") || haystack.includes("credit") || haystack.includes("balance")) return false;
if (errorInfo.status === 404 || errorInfo.status === 405) return true;
if (errorInfo.status === 400 || errorInfo.status === 415 || errorInfo.status === 422 || errorInfo.status === 501) {
return [
"unknown parameter",
"unknown url",
"unsupported",
"unsupported route",
"not found",
"no route",
"responses",
"input",
"instructions",
"previous_response_id",
"max_output_tokens",
"invalid_request_error",
"chat/completions",
].some((needle) => haystack.includes(needle));
}
return false;
}
export function hasNextAttempt(attempts: readonly OpenApiRequestAttempt[], index: number): boolean {
return index < attempts.length - 1;
}
export function payloadHasChatCompletionsShape(payload: Record<string, unknown>): boolean {
return Array.isArray(payload.choices);
}
export function forceChatCompletionsEndpoint(endpoint: string): string {
if (endpoint.endsWith("/chat/completions")) return endpoint;
if (endpoint.endsWith("/responses")) return endpoint.replace(/\/responses$/, "/chat/completions");
if (endpoint.endsWith("/v1")) return `${endpoint}/chat/completions`;
return `${endpoint.replace(/\/+$/, "")}/chat/completions`;
}
function dedupeAttempts(attempts: readonly OpenApiRequestAttempt[]): readonly OpenApiRequestAttempt[] {
const seen = new Set<string>();
const deduped: OpenApiRequestAttempt[] = [];
for (const attempt of attempts) {
const key = `${attempt.transport}:${attempt.endpoint}`;
if (seen.has(key)) continue;
seen.add(key);
deduped.push(attempt);
}
return deduped;
}
function withPathname(url: URL, pathname: string): string {
const clone = new URL(url.toString());
clone.pathname = pathname;
return clone.toString();
}
function replaceTransportPath(pathname: string, transport: OpenApiChatTransport): string {
const suffix = transport === "responses" ? "/responses" : "/chat/completions";
return pathname.replace(/\/(?:responses|chat\/completions)$/, suffix);
}
function isAzureOpenAiEndpoint(endpoint: string): boolean {
const url = safeParseUrl(endpoint);
return Boolean(url && url.hostname.endsWith(".openai.azure.com"));
}
function isOfficialOpenAiEndpoint(endpoint: string): boolean {
const url = safeParseUrl(endpoint);
return Boolean(url && url.hostname === "api.openai.com");
}
function safeParseUrl(endpoint: string): URL | undefined {
try {
return new URL(endpoint);
} catch {
return undefined;
}
}
function normalizeProviderErrorMessage(input: {
readonly endpoint: string;
readonly status: number;
readonly providerMessage: string;
readonly providerCode: string;
readonly providerType: string;
}): string {
const providerName = getEndpointProviderLabel(input.endpoint);
const normalizedProviderMessage = sanitizeProviderMessage(input.providerMessage);
const haystack = `${input.providerCode} ${input.providerType} ${normalizedProviderMessage}`.toLowerCase();
if (haystack.includes("missing authentication header") || haystack.includes("authentication header")) {
return `${providerName} says the request arrived without usable authentication. Re-save the credential in Settings after choosing the matching preset or endpoint.`;
}
if (input.status === 401 || haystack.includes("invalid api key") || haystack.includes("incorrect api key")) {
return `${providerName} rejected the saved credential. Re-save it in Settings and confirm the preset matches the endpoint.`;
}
if (isOfficialOpenAiEndpoint(input.endpoint) && (haystack.includes("insufficient_quota") || haystack.includes("current quota") || haystack.includes("billing"))) {
return "OpenAI is reporting an API billing or quota problem for this key. ChatGPT app subscriptions do not automatically cover API usage.";
}
if (haystack.includes("tokens exhausted") || haystack.includes("insufficient_quota") || haystack.includes("quota") || haystack.includes("billing") || haystack.includes("credit") || haystack.includes("balance")) {
return `${providerName} is reporting an account quota, credit, or billing problem for this credential, not a prompt-length issue.`;
}
if (input.status === 429 || haystack.includes("rate limit")) {
return `${providerName} rate-limited this request. Wait a moment and try again.`;
}
if (normalizedProviderMessage) {
return normalizedProviderMessage;
}
return `${providerName} request failed with HTTP ${input.status}.`;
}
function getEndpointProviderLabel(endpoint: string): string {
if (isAzureOpenAiEndpoint(endpoint)) return "Azure OpenAI";
const url = safeParseUrl(endpoint);
if (!url) return "OpenAPI provider";
if (url.hostname === "api.openai.com") return "OpenAI";
if (url.hostname === "openrouter.ai") return "OpenRouter";
if (url.hostname === "api.moonshot.cn") return "Moonshot";
if (url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1") return "Local OpenAPI provider";
return "OpenAPI provider";
}
function sanitizeProviderMessage(message: string): string {
return message
.replace(/\s+/g, " ")
.replace(/\s*For more information on this error, read the docs:.*$/i, "")
.trim();
}

View file

@ -0,0 +1,2 @@
export * from "./openapi-chat-provider-routing.js";
export * from "./openapi-chat-provider-payloads.js";

View file

@ -1,340 +1 @@
export type OpenApiChatTransport = "responses" | "chat-completions";
export interface OpenApiRequestAttempt {
readonly transport: OpenApiChatTransport;
readonly endpoint: string;
}
interface ProviderErrorPayload {
readonly error?: {
readonly message?: unknown;
readonly code?: unknown;
readonly type?: unknown;
};
}
export interface ProviderErrorInfo {
readonly endpoint: string;
readonly status: number;
readonly providerMessage: string;
readonly providerCode: string;
readonly providerType: string;
readonly normalizedMessage: string;
}
type ChatCompletionToolCall = {
readonly id: string;
readonly type: "function";
readonly function: {
readonly name: string;
readonly arguments: string;
};
};
export interface ChatCompletionChoice {
readonly message: {
readonly content?: string | null;
readonly tool_calls?: readonly ChatCompletionToolCall[];
};
}
export function buildRequestAttempts(endpoint: string): readonly OpenApiRequestAttempt[] {
const url = safeParseUrl(endpoint);
if (!url) {
return [{ transport: "responses", endpoint }];
}
const normalizedPath = url.pathname.replace(/\/+$/, "");
if (normalizedPath.endsWith("/chat/completions")) {
return [{ transport: "chat-completions", endpoint: url.toString() }];
}
if (normalizedPath.endsWith("/responses")) {
if (isOfficialOpenAiEndpoint(endpoint) || isAzureOpenAiEndpoint(endpoint)) {
return [{ transport: "responses", endpoint: url.toString() }];
}
return dedupeAttempts([
{ transport: "responses", endpoint: url.toString() },
{ transport: "chat-completions", endpoint: withPathname(url, replaceTransportPath(normalizedPath, "chat-completions")) },
]);
}
if (normalizedPath.endsWith("/v1")) {
return dedupeAttempts([
{ transport: "responses", endpoint: withPathname(url, `${normalizedPath}/responses`) },
{ transport: "chat-completions", endpoint: withPathname(url, `${normalizedPath}/chat/completions`) },
]);
}
return [{ transport: "responses", endpoint: url.toString() }];
}
export function parseResponsesPayload(payload: Record<string, unknown>): { readonly text: string; readonly responseId?: string } {
const text = extractResponsesOutputText(payload).trim();
const responseId = typeof payload.id === "string" && payload.id ? payload.id : undefined;
return { text, responseId };
}
export function parseChatCompletionsPayload(payload: Record<string, unknown>): { readonly text: string; readonly responseId: string } {
const text = extractChatCompletionsText(payload).trim();
const responseId = typeof payload.id === "string" && payload.id
? payload.id
: `chatcmpl-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
return { text, responseId };
}
export function buildOpenApiRequestHeaders(endpoint: string, credential: string): Record<string, string> {
const headers: Record<string, string> = {
"Content-Type": "application/json",
Authorization: `Bearer ${credential}`,
};
if (isAzureOpenAiEndpoint(endpoint)) {
headers["api-key"] = credential;
} else if (!isOfficialOpenAiEndpoint(endpoint)) {
headers["x-api-key"] = credential;
}
return headers;
}
export function getEndpointHost(endpoint: string): string {
return safeParseUrl(endpoint)?.host ?? "unknown";
}
export async function readProviderError(response: Response, endpoint: string): Promise<ProviderErrorInfo> {
let providerMessage = "";
let providerCode = "";
let providerType = "";
try {
const payload = await response.json() as ProviderErrorPayload;
providerMessage = typeof payload?.error?.message === "string" ? payload.error.message : "";
providerCode = typeof payload?.error?.code === "string" ? payload.error.code : "";
providerType = typeof payload?.error?.type === "string" ? payload.error.type : "";
} catch {
// Fall through to a generic message.
}
return {
endpoint,
status: response.status,
providerMessage,
providerCode,
providerType,
normalizedMessage: normalizeProviderErrorMessage({
endpoint,
status: response.status,
providerMessage,
providerCode,
providerType,
}),
};
}
export function shouldTryChatCompletionsFallback(attempt: OpenApiRequestAttempt, errorInfo: ProviderErrorInfo): boolean {
if (attempt.transport !== "responses") return false;
if (isOfficialOpenAiEndpoint(attempt.endpoint) || isAzureOpenAiEndpoint(attempt.endpoint)) return false;
const normalizedProviderMessage = sanitizeProviderMessage(errorInfo.providerMessage);
const haystack = `${errorInfo.providerCode} ${errorInfo.providerType} ${normalizedProviderMessage}`.toLowerCase();
if (errorInfo.status === 401 || errorInfo.status === 403 || errorInfo.status === 429) return false;
if (haystack.includes("authentication header") || haystack.includes("invalid api key") || haystack.includes("incorrect api key")) return false;
if (haystack.includes("quota") || haystack.includes("billing") || haystack.includes("credit") || haystack.includes("balance")) return false;
if (errorInfo.status === 404 || errorInfo.status === 405) return true;
if (errorInfo.status === 400 || errorInfo.status === 415 || errorInfo.status === 422 || errorInfo.status === 501) {
return [
"unknown parameter",
"unknown url",
"unsupported",
"unsupported route",
"not found",
"no route",
"responses",
"input",
"instructions",
"previous_response_id",
"max_output_tokens",
"invalid_request_error",
"chat/completions",
].some((needle) => haystack.includes(needle));
}
return false;
}
export function hasNextAttempt(attempts: readonly OpenApiRequestAttempt[], index: number): boolean {
return index < attempts.length - 1;
}
export function payloadHasChatCompletionsShape(payload: Record<string, unknown>): boolean {
return Array.isArray(payload.choices);
}
export function forceChatCompletionsEndpoint(endpoint: string): string {
if (endpoint.endsWith("/chat/completions")) return endpoint;
if (endpoint.endsWith("/responses")) return endpoint.replace(/\/responses$/, "/chat/completions");
if (endpoint.endsWith("/v1")) return `${endpoint}/chat/completions`;
return `${endpoint.replace(/\/+$/, "")}/chat/completions`;
}
export function extractChatCompletionsChoice(payload: Record<string, unknown>): ChatCompletionChoice | undefined {
const choices = Array.isArray(payload.choices) ? payload.choices : [];
const first = choices[0];
if (!first || typeof first !== "object") return undefined;
const message = (first as Record<string, unknown>).message;
if (!message || typeof message !== "object") return undefined;
const msg = message as Record<string, unknown>;
const content = typeof msg.content === "string" ? msg.content : null;
const rawToolCalls = Array.isArray(msg.tool_calls) ? msg.tool_calls : [];
const tool_calls = rawToolCalls.map((tc): ChatCompletionToolCall | null => {
if (!tc || typeof tc !== "object") return null;
const t = tc as Record<string, unknown>;
const id = typeof t.id === "string" ? t.id : "";
const type: "function" = t.type === "function" ? "function" : "function";
const fn = t.function;
if (!fn || typeof fn !== "object") return null;
const f = fn as Record<string, unknown>;
const name = typeof f.name === "string" ? f.name : "";
const args = typeof f.arguments === "string" ? f.arguments : "";
if (!id || !name) return null;
return { id, type, function: { name, arguments: args } };
}).filter((tc): tc is ChatCompletionToolCall => tc !== null);
return { message: { content, ...(tool_calls.length > 0 ? { tool_calls } : {}) } };
}
function dedupeAttempts(attempts: readonly OpenApiRequestAttempt[]): readonly OpenApiRequestAttempt[] {
const seen = new Set<string>();
const deduped: OpenApiRequestAttempt[] = [];
for (const attempt of attempts) {
const key = `${attempt.transport}:${attempt.endpoint}`;
if (seen.has(key)) continue;
seen.add(key);
deduped.push(attempt);
}
return deduped;
}
function withPathname(url: URL, pathname: string): string {
const clone = new URL(url.toString());
clone.pathname = pathname;
return clone.toString();
}
function replaceTransportPath(pathname: string, transport: OpenApiChatTransport): string {
const suffix = transport === "responses" ? "/responses" : "/chat/completions";
return pathname.replace(/\/(?:responses|chat\/completions)$/, suffix);
}
function isAzureOpenAiEndpoint(endpoint: string): boolean {
const url = safeParseUrl(endpoint);
return Boolean(url && url.hostname.endsWith(".openai.azure.com"));
}
function isOfficialOpenAiEndpoint(endpoint: string): boolean {
const url = safeParseUrl(endpoint);
return Boolean(url && url.hostname === "api.openai.com");
}
function safeParseUrl(endpoint: string): URL | undefined {
try {
return new URL(endpoint);
} catch {
return undefined;
}
}
function normalizeProviderErrorMessage(input: {
readonly endpoint: string;
readonly status: number;
readonly providerMessage: string;
readonly providerCode: string;
readonly providerType: string;
}): string {
const providerName = getEndpointProviderLabel(input.endpoint);
const normalizedProviderMessage = sanitizeProviderMessage(input.providerMessage);
const haystack = `${input.providerCode} ${input.providerType} ${normalizedProviderMessage}`.toLowerCase();
if (haystack.includes("missing authentication header") || haystack.includes("authentication header")) {
return `${providerName} says the request arrived without usable authentication. Re-save the credential in Settings after choosing the matching preset or endpoint.`;
}
if (input.status === 401 || haystack.includes("invalid api key") || haystack.includes("incorrect api key")) {
return `${providerName} rejected the saved credential. Re-save it in Settings and confirm the preset matches the endpoint.`;
}
if (isOfficialOpenAiEndpoint(input.endpoint) && (haystack.includes("insufficient_quota") || haystack.includes("current quota") || haystack.includes("billing"))) {
return "OpenAI is reporting an API billing or quota problem for this key. ChatGPT app subscriptions do not automatically cover API usage.";
}
if (haystack.includes("tokens exhausted") || haystack.includes("insufficient_quota") || haystack.includes("quota") || haystack.includes("billing") || haystack.includes("credit") || haystack.includes("balance")) {
return `${providerName} is reporting an account quota, credit, or billing problem for this credential, not a prompt-length issue.`;
}
if (input.status === 429 || haystack.includes("rate limit")) {
return `${providerName} rate-limited this request. Wait a moment and try again.`;
}
if (normalizedProviderMessage) {
return normalizedProviderMessage;
}
return `${providerName} request failed with HTTP ${input.status}.`;
}
function getEndpointProviderLabel(endpoint: string): string {
if (isAzureOpenAiEndpoint(endpoint)) return "Azure OpenAI";
const url = safeParseUrl(endpoint);
if (!url) return "OpenAPI provider";
if (url.hostname === "api.openai.com") return "OpenAI";
if (url.hostname === "openrouter.ai") return "OpenRouter";
if (url.hostname === "api.moonshot.cn") return "Moonshot";
if (url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1") return "Local OpenAPI provider";
return "OpenAPI provider";
}
function sanitizeProviderMessage(message: string): string {
return message
.replace(/\s+/g, " ")
.replace(/\s*For more information on this error, read the docs:.*$/i, "")
.trim();
}
function extractResponsesOutputText(payload: Record<string, unknown>): string {
if (typeof payload.output_text === "string") return payload.output_text;
const output = Array.isArray(payload.output) ? payload.output : [];
const parts: string[] = [];
for (const item of output) {
if (!isRecord(item) || item.type !== "message" || !Array.isArray(item.content)) continue;
for (const contentPart of item.content) {
if (!isRecord(contentPart) || contentPart.type !== "output_text" || typeof contentPart.text !== "string") continue;
parts.push(contentPart.text);
}
}
return parts.join("");
}
function extractChatCompletionsText(payload: Record<string, unknown>): string {
const choices = Array.isArray(payload.choices) ? payload.choices : [];
const parts: string[] = [];
for (const choice of choices) {
if (!isRecord(choice) || !isRecord(choice.message)) continue;
const content = choice.message.content;
if (typeof content === "string") {
parts.push(content);
continue;
}
if (!Array.isArray(content)) continue;
for (const part of content) {
if (!isRecord(part)) continue;
if (typeof part.text === "string") {
parts.push(part.text);
} else if (isRecord(part.text) && typeof part.text.value === "string") {
parts.push(part.text.value);
}
}
}
return parts.join("");
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
export * from "./openapi-chat-provider-support.js";

View file

@ -16,11 +16,20 @@ const desktopRoot = process.env.FAMILIAROS_DESKTOP_ROOT ?? resolve(dirname(fileU
const openApiChatSource = readFileSync(resolve(desktopRoot, "src/openapi-chat.ts"), "utf8");
const openApiChatPromptFlowsSource = readFileSync(resolve(desktopRoot, "src/openapi-chat-prompt-flows.ts"), "utf8");
const openApiChatProviderSource = readFileSync(resolve(desktopRoot, "src/openapi-chat-provider.ts"), "utf8");
const openApiChatProviderSupportSource = readFileSync(resolve(desktopRoot, "src/openapi-chat-provider-support.ts"), "utf8");
const openApiChatProviderRoutingSource = readFileSync(resolve(desktopRoot, "src/openapi-chat-provider-routing.ts"), "utf8");
const openApiChatProviderPayloadsSource = readFileSync(resolve(desktopRoot, "src/openapi-chat-provider-payloads.ts"), "utf8");
assert.match(openApiChatSource, /from "\.\/openapi-chat-prompt-flows(?:\.js)?"/, "openapi-chat must import the extracted prompt-flow seam.");
assert.match(openApiChatPromptFlowsSource, /from "\.\/openapi-chat-provider(?:\.js)?"/, "openapi-chat prompt flows must import the extracted provider helper module.");
assert.match(openApiChatProviderSource, /export function buildRequestAttempts/, "openapi-chat-provider must export request-attempt routing.");
assert.match(openApiChatProviderSource, /export async function readProviderError/, "openapi-chat-provider must export provider error normalization.");
assert.match(openApiChatProviderSource, /export \* from "\.\/openapi-chat-provider-support(?:\.js)?"/, "openapi-chat-provider must re-export the extracted provider support seam.");
assert.match(openApiChatProviderSupportSource, /export \* from "\.\/openapi-chat-provider-routing(?:\.js)?"/, "openapi-chat provider support seam must re-export routing helpers.");
assert.match(openApiChatProviderSupportSource, /export \* from "\.\/openapi-chat-provider-payloads(?:\.js)?"/, "openapi-chat provider support seam must re-export payload helpers.");
assert.match(openApiChatProviderRoutingSource, /export function buildRequestAttempts/, "openapi-chat provider routing seam must export request-attempt routing.");
assert.match(openApiChatProviderRoutingSource, /export async function readProviderError/, "openapi-chat provider routing seam must export provider error normalization.");
assert.match(openApiChatProviderPayloadsSource, /export function parseResponsesPayload/, "openapi-chat provider payload seam must export responses payload parsing.");
assert.match(openApiChatProviderPayloadsSource, /export function parseChatCompletionsPayload/, "openapi-chat provider payload seam must export chat-completions payload parsing.");
assert.match(openApiChatProviderPayloadsSource, /export function extractChatCompletionsChoice/, "openapi-chat provider payload seam must export tool-call extraction.");
assert.deepEqual(
buildRequestAttempts("https://openrouter.ai/api/v1/responses"),