UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

187 lines (186 loc) • 10.9 kB
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 }, }); }