mirror of
https://github.com/fosrl/pangolin.git
synced 2026-08-14 08:19:51 +02:00
format custom error codes according to capability
This commit is contained in:
@@ -8,8 +8,11 @@ export type AiCapabilityRoute = {
|
||||
path: string;
|
||||
};
|
||||
|
||||
export type AiProtocolFamily = "openai" | "anthropic" | "google" | "bedrock";
|
||||
|
||||
export type AiCapabilityDefinition = {
|
||||
id: AiCapability;
|
||||
protocolFamily: AiProtocolFamily;
|
||||
routes: AiCapabilityRoute[];
|
||||
extractModel: (req: Request) => string | undefined;
|
||||
resolveUpstreamUrl: (
|
||||
@@ -18,41 +21,8 @@ export type AiCapabilityDefinition = {
|
||||
model: string
|
||||
) => string;
|
||||
isStreaming: (req: Request, contentType: string) => boolean;
|
||||
/** Protocol-shaped body returned when gateway auth fails for this capability. */
|
||||
authErrorBody: Record<string, unknown>;
|
||||
};
|
||||
|
||||
const AUTH_MESSAGE = "Invalid API key provided.";
|
||||
|
||||
export const OPENAI_AUTH_ERROR_BODY = {
|
||||
error: {
|
||||
message: AUTH_MESSAGE,
|
||||
type: "authentication_error",
|
||||
param: null,
|
||||
code: "invalid_api_key"
|
||||
}
|
||||
} as const;
|
||||
|
||||
const ANTHROPIC_AUTH_ERROR_BODY = {
|
||||
type: "error",
|
||||
error: {
|
||||
type: "authentication_error",
|
||||
message: AUTH_MESSAGE
|
||||
}
|
||||
} as const;
|
||||
|
||||
const GOOGLE_AUTH_ERROR_BODY = {
|
||||
error: {
|
||||
code: 401,
|
||||
message: AUTH_MESSAGE,
|
||||
status: "UNAUTHENTICATED"
|
||||
}
|
||||
} as const;
|
||||
|
||||
const BEDROCK_AUTH_ERROR_BODY = {
|
||||
message: "The security token included in the request is invalid."
|
||||
} as const;
|
||||
|
||||
function bodyModel(req: Request): string | undefined {
|
||||
return typeof req.body?.model === "string" ? req.body.model : undefined;
|
||||
}
|
||||
@@ -137,6 +107,7 @@ export const AI_CAPABILITY_DEFS: Record<AiCapability, AiCapabilityDefinition> =
|
||||
{
|
||||
openai_chat: {
|
||||
id: "openai_chat",
|
||||
protocolFamily: "openai",
|
||||
routes: [
|
||||
{ method: "POST", path: "/v1/chat/completions" },
|
||||
{ method: "POST", path: "/chat/completions" }
|
||||
@@ -144,29 +115,29 @@ export const AI_CAPABILITY_DEFS: Record<AiCapability, AiCapabilityDefinition> =
|
||||
extractModel: bodyModel,
|
||||
resolveUpstreamUrl: (base, req) =>
|
||||
joinUpstreamUrl(base, pathFromRequest(req)),
|
||||
isStreaming: isBodyOrSseStreaming,
|
||||
authErrorBody: OPENAI_AUTH_ERROR_BODY
|
||||
isStreaming: isBodyOrSseStreaming
|
||||
},
|
||||
openai_responses: {
|
||||
id: "openai_responses",
|
||||
protocolFamily: "openai",
|
||||
routes: [{ method: "POST", path: "/v1/responses" }],
|
||||
extractModel: bodyModel,
|
||||
resolveUpstreamUrl: (base, req) =>
|
||||
joinUpstreamUrl(base, pathFromRequest(req)),
|
||||
isStreaming: isBodyOrSseStreaming,
|
||||
authErrorBody: OPENAI_AUTH_ERROR_BODY
|
||||
isStreaming: isBodyOrSseStreaming
|
||||
},
|
||||
anthropic_messages: {
|
||||
id: "anthropic_messages",
|
||||
protocolFamily: "anthropic",
|
||||
routes: [{ method: "POST", path: "/v1/messages" }],
|
||||
extractModel: bodyModel,
|
||||
resolveUpstreamUrl: (base, req) =>
|
||||
joinUpstreamUrl(base, pathFromRequest(req)),
|
||||
isStreaming: isBodyOrSseStreaming,
|
||||
authErrorBody: ANTHROPIC_AUTH_ERROR_BODY
|
||||
isStreaming: isBodyOrSseStreaming
|
||||
},
|
||||
gemini_generate_content: {
|
||||
id: "gemini_generate_content",
|
||||
protocolFamily: "google",
|
||||
routes: [
|
||||
{
|
||||
method: "POST",
|
||||
@@ -180,11 +151,11 @@ export const AI_CAPABILITY_DEFS: Record<AiCapability, AiCapabilityDefinition> =
|
||||
extractModel: paramModel,
|
||||
resolveUpstreamUrl: (base, req) =>
|
||||
joinUpstreamUrl(base, pathFromRequest(req)),
|
||||
isStreaming: isGeminiStyleStreaming,
|
||||
authErrorBody: GOOGLE_AUTH_ERROR_BODY
|
||||
isStreaming: isGeminiStyleStreaming
|
||||
},
|
||||
google_generate_content: {
|
||||
id: "google_generate_content",
|
||||
protocolFamily: "google",
|
||||
routes: [
|
||||
{
|
||||
method: "POST",
|
||||
@@ -199,11 +170,11 @@ export const AI_CAPABILITY_DEFS: Record<AiCapability, AiCapabilityDefinition> =
|
||||
extractModel: paramModel,
|
||||
resolveUpstreamUrl: (base, req) =>
|
||||
joinUpstreamUrl(base, pathFromRequest(req)),
|
||||
isStreaming: isGeminiStyleStreaming,
|
||||
authErrorBody: GOOGLE_AUTH_ERROR_BODY
|
||||
isStreaming: isGeminiStyleStreaming
|
||||
},
|
||||
google_raw_predict: {
|
||||
id: "google_raw_predict",
|
||||
protocolFamily: "google",
|
||||
routes: [
|
||||
{
|
||||
method: "POST",
|
||||
@@ -220,11 +191,11 @@ export const AI_CAPABILITY_DEFS: Record<AiCapability, AiCapabilityDefinition> =
|
||||
isStreaming: (req, contentType) =>
|
||||
pathIncludes(req, "streamRawPredict") ||
|
||||
pathIncludes(req, "alt=sse") ||
|
||||
contentTypeIsSse(contentType),
|
||||
authErrorBody: GOOGLE_AUTH_ERROR_BODY
|
||||
contentTypeIsSse(contentType)
|
||||
},
|
||||
bedrock_model_invoke: {
|
||||
id: "bedrock_model_invoke",
|
||||
protocolFamily: "bedrock",
|
||||
routes: [
|
||||
{ method: "POST", path: "/model/:model/invoke" },
|
||||
{
|
||||
@@ -238,11 +209,11 @@ export const AI_CAPABILITY_DEFS: Record<AiCapability, AiCapabilityDefinition> =
|
||||
isStreaming: (req, contentType) =>
|
||||
pathIncludes(req, "invoke-with-response-stream") ||
|
||||
contentTypeIsAmazonEventStream(contentType) ||
|
||||
contentTypeIsSse(contentType),
|
||||
authErrorBody: BEDROCK_AUTH_ERROR_BODY
|
||||
contentTypeIsSse(contentType)
|
||||
},
|
||||
bedrock_converse: {
|
||||
id: "bedrock_converse",
|
||||
protocolFamily: "bedrock",
|
||||
routes: [
|
||||
{ method: "POST", path: "/model/:model/converse" },
|
||||
{ method: "POST", path: "/model/:model/converse-stream" }
|
||||
@@ -253,8 +224,7 @@ export const AI_CAPABILITY_DEFS: Record<AiCapability, AiCapabilityDefinition> =
|
||||
isStreaming: (req, contentType) =>
|
||||
pathIncludes(req, "converse-stream") ||
|
||||
contentTypeIsAmazonEventStream(contentType) ||
|
||||
contentTypeIsSse(contentType),
|
||||
authErrorBody: BEDROCK_AUTH_ERROR_BODY
|
||||
contentTypeIsSse(contentType)
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
AI_CAPABILITY_DEFS,
|
||||
OPENAI_AUTH_ERROR_BODY,
|
||||
type AiCapability
|
||||
type AiCapability,
|
||||
type AiProtocolFamily
|
||||
} from "@server/lib/aiCapabilities";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
|
||||
@@ -11,17 +11,129 @@ export type ClientErrorResponse = {
|
||||
body: string;
|
||||
};
|
||||
|
||||
export type AiCapabilityErrorKind =
|
||||
| "authentication"
|
||||
| "invalid_request"
|
||||
| "not_found"
|
||||
| "permission"
|
||||
| "rate_limit"
|
||||
| "internal";
|
||||
|
||||
const AUTH_MESSAGE = "Invalid API key provided.";
|
||||
|
||||
type KindFields = {
|
||||
openaiType: string;
|
||||
openaiCode: string | null;
|
||||
anthropicType: string;
|
||||
googleStatus: string;
|
||||
};
|
||||
|
||||
const KIND_FIELDS: Record<AiCapabilityErrorKind, KindFields> = {
|
||||
authentication: {
|
||||
openaiType: "authentication_error",
|
||||
openaiCode: "invalid_api_key",
|
||||
anthropicType: "authentication_error",
|
||||
googleStatus: "UNAUTHENTICATED"
|
||||
},
|
||||
invalid_request: {
|
||||
openaiType: "invalid_request_error",
|
||||
openaiCode: null,
|
||||
anthropicType: "invalid_request_error",
|
||||
googleStatus: "INVALID_ARGUMENT"
|
||||
},
|
||||
not_found: {
|
||||
openaiType: "invalid_request_error",
|
||||
openaiCode: null,
|
||||
anthropicType: "not_found_error",
|
||||
googleStatus: "NOT_FOUND"
|
||||
},
|
||||
permission: {
|
||||
openaiType: "invalid_request_error",
|
||||
openaiCode: null,
|
||||
anthropicType: "permission_error",
|
||||
googleStatus: "PERMISSION_DENIED"
|
||||
},
|
||||
rate_limit: {
|
||||
openaiType: "rate_limit_error",
|
||||
openaiCode: "rate_limit_exceeded",
|
||||
anthropicType: "rate_limit_error",
|
||||
googleStatus: "RESOURCE_EXHAUSTED"
|
||||
},
|
||||
internal: {
|
||||
openaiType: "api_error",
|
||||
openaiCode: null,
|
||||
anthropicType: "api_error",
|
||||
googleStatus: "INTERNAL"
|
||||
}
|
||||
};
|
||||
|
||||
function resolveProtocolFamily(
|
||||
capability: AiCapability | null
|
||||
): AiProtocolFamily {
|
||||
if (capability == null) {
|
||||
return "openai";
|
||||
}
|
||||
return AI_CAPABILITY_DEFS[capability].protocolFamily;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a protocol-native error body for the given capability.
|
||||
* Message stays contextual; only the envelope/machine fields follow the
|
||||
* capability's native API shape.
|
||||
*/
|
||||
export function buildAiCapabilityErrorBody(
|
||||
capability: AiCapability | null,
|
||||
kind: AiCapabilityErrorKind,
|
||||
message: string,
|
||||
httpStatus?: number
|
||||
): Record<string, unknown> {
|
||||
const family = resolveProtocolFamily(capability);
|
||||
const fields = KIND_FIELDS[kind];
|
||||
|
||||
switch (family) {
|
||||
case "openai":
|
||||
return {
|
||||
error: {
|
||||
message,
|
||||
type: fields.openaiType,
|
||||
param: null,
|
||||
code: fields.openaiCode
|
||||
}
|
||||
};
|
||||
case "anthropic":
|
||||
return {
|
||||
type: "error",
|
||||
error: {
|
||||
type: fields.anthropicType,
|
||||
message
|
||||
}
|
||||
};
|
||||
case "google":
|
||||
return {
|
||||
error: {
|
||||
code: httpStatus ?? HttpCode.BAD_REQUEST,
|
||||
message,
|
||||
status: fields.googleStatus
|
||||
}
|
||||
};
|
||||
case "bedrock":
|
||||
return { message };
|
||||
}
|
||||
}
|
||||
|
||||
export function buildInferenceAuthClientError(
|
||||
capability: AiCapability | null
|
||||
): ClientErrorResponse {
|
||||
const body =
|
||||
capability != null
|
||||
? AI_CAPABILITY_DEFS[capability].authErrorBody
|
||||
: OPENAI_AUTH_ERROR_BODY;
|
||||
|
||||
return {
|
||||
statusCode: HttpCode.UNAUTHORIZED,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(body)
|
||||
body: JSON.stringify(
|
||||
buildAiCapabilityErrorBody(
|
||||
capability,
|
||||
"authentication",
|
||||
AUTH_MESSAGE,
|
||||
HttpCode.UNAUTHORIZED
|
||||
)
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user