@sema-agent/core
Version:
Stateless, task-oriented AI agent core
187 lines (186 loc) • 10.9 kB
JavaScript
import { BrainError, carriedProviderWord, providerErrorWord, statedEnvelope } from "./errors.js";
import { readUnifiedResetEpochMs } from "./retry.js";
export const PROVIDER_CONDITIONS = Object.freeze(["quota_exhausted", "throttled", "overloaded", "stop_other", "unknown"]);
export const PROVIDER_CONDITION_SOURCES = Object.freeze(["unified_header", "provider_code", "status"]);
export const PROVIDER_CONDITION_TRAITS = {
quota_exhausted: { retryable: false, inBandCode: "rate_limit" },
throttled: { retryable: true, inBandCode: "rate_limit" },
overloaded: { retryable: true, inBandCode: "server" },
stop_other: { retryable: false, inBandCode: "invalid_request" },
unknown: { retryable: true, inBandCode: "server" },
};
Object.freeze(PROVIDER_CONDITION_TRAITS);
export function isProviderCondition(v) {
return typeof v === "string" && PROVIDER_CONDITIONS.includes(v);
}
export function isProviderConditionSource(v) {
return typeof v === "string" && PROVIDER_CONDITION_SOURCES.includes(v);
}
export const INPUT_TOO_LONG_ANCHORED_RE = /^(?:prompt is too long|prompt too long|input is too long|this model's maximum context length is |context.{0,3}length|context window (?:limit|exceeded)|reached its context window|too many tokens|maximum context|input length and `max_tokens` exceed context limit|(?:the )?request(?: \([^)]{0,40}\))? exceeds|(?:the )?input token count(?: \([^)]{0,40}\))? exceeds|requested tokens(?: \([^)]{0,40}\))? exceed|your input exceeds|(?:the )?number of tokens in the request exceeds|range of input length should be|input token length too long|invalid request: your request exceeded model token limit|total message size)/i;
export function startsWithInputTooLongAnchor(diagnostic) {
return INPUT_TOO_LONG_ANCHORED_RE.test(diagnostic.trimStart());
}
const INPUT_TOO_LONG_STATUSES = new Set([400, 413, 422]);
const OPENAI_CHAT = "openai-completions";
const OPENAI_RESPONSES = "openai-responses";
const ANTHROPIC = "anthropic-messages";
const OPENAI_SPEND_CODES = ["credit_balance_exhausted", "organization_spend_limit_exceeded", "project_spend_limit_exceeded", "organization_usage_limit_exceeded"];
export const PROVIDER_DIALECTS = Object.freeze([
{
name: "deepseek",
key: { wires: [OPENAI_CHAT, ANTHROPIC], hosts: ["api.deepseek.com"] },
failures: [
{ status: [429], condition: "throttled" },
{ status: [503], condition: "overloaded" },
],
},
{
name: "dashscope",
key: { wires: [OPENAI_CHAT], hosts: ["dashscope.aliyuncs.com", "dashscope-intl.aliyuncs.com"] },
failures: [
{ status: [429], word: ["limit_requests", "limit_burst_rate", "Throttling.Concurrency", "Throttling"], condition: "throttled" },
{ status: [429], word: ["insufficient_quota", "Throttling.AllocationQuota"], sentence: /^free allocated quota exceeded/i, condition: "quota_exhausted" },
{ status: [429], word: ["insufficient_quota", "Throttling.AllocationQuota"], condition: "throttled" },
{ status: [429], word: ["Throttling.ServiceOverloaded", "ResourceExhausted"], condition: "overloaded" },
{ status: [429], word: ["PrepaidBillOverdue", "PostpaidBillOverdue", "BudgetLimitExceeded", "CommodityNotPurchased"], condition: "quota_exhausted" },
{ status: [429], condition: "throttled" },
{ status: [400], word: ["Arrearage"], condition: "quota_exhausted" },
{ status: [403], word: ["AllocationQuota.FreeTierOnly"], condition: "quota_exhausted" },
{ status: [400], word: ["DataInspectionFailed"], condition: "stop_other" },
{ status: [500, 503], word: ["InternalError", "RequestTimeOut", "ModelUnavailable"], condition: "overloaded" },
],
},
{
name: "moonshot",
key: { wires: [OPENAI_CHAT], hosts: ["api.moonshot.cn", "api.moonshot.ai"] },
failures: [
{ status: [429], word: ["exceeded_current_quota_error"], condition: "quota_exhausted" },
{ status: [429], word: ["rate_limit_reached_error"], sentence: /\bTPD\b|每日/i, condition: "quota_exhausted" },
{ status: [429], word: ["rate_limit_reached_error"], condition: "throttled" },
{ status: [429], word: ["engine_overloaded_error"], condition: "overloaded" },
{ status: [429], condition: "throttled" },
{ status: [503], word: ["server_unavailable"], condition: "overloaded" },
{ status: [403], word: ["permission_denied_error"], condition: "stop_other" },
{ status: [400], word: ["content_filter"], condition: "stop_other" },
],
},
{
name: "kimi-code",
key: { wires: [ANTHROPIC, OPENAI_CHAT], hosts: ["api.kimi.com"], path: "/coding" },
failures: [
{ status: [429], sentence: /^the engine is currently overloaded/i, condition: "overloaded" },
{ status: [429], condition: "throttled" },
{ status: [403], sentence: /^you(?:'|’)ve reached your\b.*\busage limit/i, condition: "quota_exhausted" },
],
},
{
name: "glm",
key: { wires: [OPENAI_CHAT, ANTHROPIC], hosts: ["open.bigmodel.cn", "api.z.ai"] },
failures: [
{ status: [429], word: ["1302"], condition: "throttled" },
{ status: [429], word: ["1305"], condition: "overloaded" },
{
status: [429],
word: ["1113", "1308", "1309", "1310", "1314", "1315", "1316", "1317", "1318", "1319", "1320", "1321"],
condition: "quota_exhausted",
},
{ status: [429], word: ["1311", "1313"], condition: "stop_other" },
{ status: [400], word: ["1261"], condition: "stop_other", inputTooLong: true },
],
},
{
name: "openai",
key: { wires: [OPENAI_CHAT, OPENAI_RESPONSES], hosts: ["api.openai.com"] },
failures: [
{ status: [429], word: [...OPENAI_SPEND_CODES, "insufficient_quota"], condition: "quota_exhausted" },
{ status: [429], word: ["slow_down", "rate_limit_exceeded", "rate_limit_error"], condition: "throttled" },
{ status: [429], condition: "throttled" },
{ status: [503], word: ["server_is_overloaded"], condition: "overloaded" },
],
},
]);
export const GENERIC_FAILURES = Object.freeze([
{ status: [402], condition: "quota_exhausted" },
{ status: [429], word: [...OPENAI_SPEND_CODES], condition: "quota_exhausted" },
{ status: [429], word: ["insufficient_quota"], condition: "unknown" },
{ status: [400, 422], word: ["context_length_exceeded"], condition: "stop_other", inputTooLong: true },
{ status: [400, 422], sentence: INPUT_TOO_LONG_ANCHORED_RE, condition: "stop_other", inputTooLong: true },
{ status: [413], condition: "stop_other", inputTooLong: true },
]);
export function isTransientStatus(status) {
return status >= 500 || status === 408 || status === 409;
}
export function resolveProviderDialect(url, wire) {
if (!URL.canParse(url))
return undefined;
const u = new URL(url);
const host = u.hostname.toLowerCase().replace(/\.$/, "");
const path = u.pathname;
return PROVIDER_DIALECTS.find((d) => d.key.wires.includes(wire) &&
d.key.hosts.some((h) => host === h || host.endsWith(`.${h}`)) &&
(d.key.path === undefined || path === d.key.path || path.startsWith(`${d.key.path}/`)));
}
function statementOf(err) {
if (err === undefined)
return undefined;
const word = providerErrorWord(err);
const sentence = typeof err.message === "string" ? err.message.trimStart() : undefined;
return { ...(word !== undefined ? { word } : {}), ...(sentence !== undefined ? { sentence } : {}) };
}
function bestRow(rows, status, stated) {
let best;
let bestRank = -1;
for (const row of rows) {
if (status === undefined ? row.word === undefined && row.sentence === undefined : row.status !== undefined && !row.status.includes(status))
continue;
if (row.word !== undefined && (stated?.word === undefined || !row.word.includes(stated.word)))
continue;
if (row.sentence !== undefined && (stated?.sentence === undefined || !row.sentence.test(stated.sentence)))
continue;
const rank = (row.word !== undefined ? 2 : 0) + (row.sentence !== undefined ? 1 : 0);
if (rank > bestRank) {
best = row;
bestRank = rank;
}
}
return best;
}
function verdictOf(dialect, status, stated, enveloped) {
const row = bestRow(dialect?.failures ?? [], status, stated) ?? bestRow(GENERIC_FAILURES, status, stated);
const condition = row?.condition ?? (status === undefined || status === 429 || isTransientStatus(status) ? "unknown" : "stop_other");
const source = row !== undefined && (row.word !== undefined || row.sentence !== undefined) ? "provider_code" : "status";
const inputTooLong = status !== undefined && !INPUT_TOO_LONG_STATUSES.has(status) ? "no" : row?.inputTooLong === true ? "yes" : enveloped ? "no" : "unknown";
return { condition, source, inputTooLong };
}
const UNIFIED_QUOTA_HEADERS = [
"anthropic-ratelimit-unified-representative-claim",
"anthropic-ratelimit-unified-overage-status",
"anthropic-ratelimit-unified-overage-disabled-reason",
];
function headerPresent(headers, name) {
const v = headers?.get?.(name);
return typeof v === "string" && v.length > 0;
}
export function judgeHttpFailure(args) {
const { dialect, status, headers, body } = args;
if (status === 429 && UNIFIED_QUOTA_HEADERS.some((h) => headerPresent(headers, h))) {
const resetAtMs = readUnifiedResetEpochMs(headers);
return { condition: "quota_exhausted", source: "unified_header", ...(resetAtMs !== undefined ? { resetAtMs } : {}), inputTooLong: "no" };
}
const envelope = statedEnvelope(body);
return verdictOf(dialect, status, statementOf(envelope), envelope !== undefined);
}
export function judgeInBandError(dialect, envelope) {
return verdictOf(dialect, undefined, statementOf(envelope), true);
}
export function inBandProviderError(dialect, envelope, label) {
const v = judgeInBandError(dialect, envelope);
const word = carriedProviderWord(envelope);
const sentence = typeof envelope.message === "string" ? envelope.message : "";
return new BrainError(PROVIDER_CONDITION_TRAITS[v.condition].inBandCode, `${label} stream error (${word ?? "unknown"}): ${sentence}`, undefined, {
apiError: true,
...(v.inputTooLong === "yes" ? { inputTooLong: true } : v.inputTooLong === "no" ? { inputTooLongRuledOut: true } : {}),
...(word !== undefined ? { providerCode: word } : {}),
verdict: { condition: v.condition, source: v.source },
});
}