@starlink-awaken/agentmesh 1.0.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/CHANGELOG.md +60 -41
  2. package/README.zh-CN.md +137 -167
  3. package/config/gateway.yaml +78 -0
  4. package/dist/src/adapters/base.d.ts +22 -0
  5. package/dist/src/adapters/base.js +10 -0
  6. package/dist/src/adapters/claude-code.d.ts +22 -0
  7. package/dist/src/adapters/claude-code.js +112 -0
  8. package/dist/src/adapters/openclaw.d.ts +22 -0
  9. package/dist/src/adapters/openclaw.js +110 -0
  10. package/dist/src/adapters/process.d.ts +28 -0
  11. package/dist/src/adapters/process.js +121 -0
  12. package/dist/src/cli/connect.d.ts +26 -0
  13. package/dist/src/cli/connect.js +544 -0
  14. package/dist/src/cli/setup.d.ts +2 -0
  15. package/dist/src/cli/setup.js +97 -0
  16. package/dist/src/cli.d.ts +2 -0
  17. package/dist/src/cli.js +410 -0
  18. package/dist/src/core/agent-registry.d.ts +48 -0
  19. package/dist/src/core/agent-registry.js +295 -0
  20. package/dist/src/core/config.d.ts +59 -0
  21. package/dist/src/core/config.js +101 -0
  22. package/dist/src/core/context-manager.d.ts +52 -0
  23. package/dist/src/core/context-manager.js +165 -0
  24. package/dist/src/core/event-bus.d.ts +35 -0
  25. package/dist/src/core/event-bus.js +62 -0
  26. package/dist/src/core/logger.d.ts +14 -0
  27. package/dist/src/core/logger.js +57 -0
  28. package/dist/src/core/metrics.d.ts +87 -0
  29. package/dist/src/core/metrics.js +167 -0
  30. package/dist/src/core/router.d.ts +46 -0
  31. package/dist/src/core/router.js +90 -0
  32. package/dist/src/core/task-manager.d.ts +41 -0
  33. package/dist/src/core/task-manager.js +197 -0
  34. package/dist/src/core/vector-store.d.ts +37 -0
  35. package/dist/src/core/vector-store.js +175 -0
  36. package/dist/src/index.d.ts +1 -0
  37. package/dist/src/index.js +105 -0
  38. package/dist/src/model-gateway/circuit-breaker.d.ts +21 -0
  39. package/dist/src/model-gateway/circuit-breaker.js +86 -0
  40. package/dist/src/model-gateway/health.d.ts +12 -0
  41. package/dist/src/model-gateway/health.js +80 -0
  42. package/dist/src/model-gateway/providers.d.ts +4 -0
  43. package/dist/src/model-gateway/providers.js +113 -0
  44. package/dist/src/model-gateway/quota.d.ts +4 -0
  45. package/dist/src/model-gateway/quota.js +107 -0
  46. package/dist/src/model-gateway/rate-limit.d.ts +12 -0
  47. package/dist/src/model-gateway/rate-limit.js +51 -0
  48. package/dist/src/model-gateway/retry.d.ts +14 -0
  49. package/dist/src/model-gateway/retry.js +48 -0
  50. package/dist/src/model-gateway/router.d.ts +4 -0
  51. package/dist/src/model-gateway/router.js +79 -0
  52. package/dist/src/model-gateway/routes.d.ts +2 -0
  53. package/dist/src/model-gateway/routes.js +172 -0
  54. package/dist/src/model-gateway/types.d.ts +47 -0
  55. package/dist/src/model-gateway/types.js +1 -0
  56. package/dist/src/routes/api.d.ts +2 -0
  57. package/dist/src/routes/api.js +128 -0
  58. package/dist/src/routes/websocket.d.ts +2 -0
  59. package/dist/src/routes/websocket.js +64 -0
  60. package/dist/src/types/index.d.ts +71 -0
  61. package/dist/src/types/index.js +1 -0
  62. package/dist/tests/core/context-manager.test.d.ts +1 -0
  63. package/dist/tests/core/context-manager.test.js +35 -0
  64. package/dist/tests/core/router.test.d.ts +1 -0
  65. package/dist/tests/core/router.test.js +79 -0
  66. package/dist/tests/model-gateway/circuit-breaker.test.d.ts +1 -0
  67. package/dist/tests/model-gateway/circuit-breaker.test.js +84 -0
  68. package/dist/tests/model-gateway/providers.test.d.ts +1 -0
  69. package/dist/tests/model-gateway/providers.test.js +80 -0
  70. package/dist/tests/model-gateway/quota.test.d.ts +1 -0
  71. package/dist/tests/model-gateway/quota.test.js +60 -0
  72. package/dist/tests/model-gateway/rate-limit.test.d.ts +1 -0
  73. package/dist/tests/model-gateway/rate-limit.test.js +42 -0
  74. package/dist/tests/model-gateway/retry.test.d.ts +1 -0
  75. package/dist/tests/model-gateway/retry.test.js +47 -0
  76. package/dist/tests/model-gateway/router.test.d.ts +1 -0
  77. package/dist/tests/model-gateway/router.test.js +108 -0
  78. package/dist/tests/model-gateway/routes.test.d.ts +1 -0
  79. package/dist/tests/model-gateway/routes.test.js +83 -0
  80. package/docs/api.md +187 -460
  81. package/docs/architecture.md +138 -0
  82. package/docs/configuration.md +188 -0
  83. package/package.json +3 -1
@@ -0,0 +1,80 @@
1
+ import { circuitBreakerRegistry } from './circuit-breaker.js';
2
+ const healthCache = new Map();
3
+ const CACHE_TTL = 30_000; // 30s
4
+ export async function checkProviderHealth(provider) {
5
+ const cached = healthCache.get(provider.name);
6
+ if (cached && Date.now() - cached.ts < CACHE_TTL) {
7
+ return { ...cached.result, checked_at: new Date(cached.result.checked_at).toISOString() };
8
+ }
9
+ const circuit = circuitBreakerRegistry.getState(provider.name);
10
+ if (circuit === 'OPEN') {
11
+ const result = {
12
+ provider: provider.name,
13
+ status: 'unhealthy',
14
+ latency_ms: 0,
15
+ circuit,
16
+ error: 'circuit_breaker_open',
17
+ checked_at: new Date().toISOString(),
18
+ };
19
+ healthCache.set(provider.name, { result, ts: Date.now() });
20
+ return result;
21
+ }
22
+ const start = Date.now();
23
+ try {
24
+ const resp = await fetch(`${provider.base_url.replace(/\/$/, '')}/models`, {
25
+ headers: { Authorization: `Bearer ${provider.api_key}` },
26
+ signal: AbortSignal.timeout(5000),
27
+ });
28
+ const latency = Date.now() - start;
29
+ const result = {
30
+ provider: provider.name,
31
+ status: resp.ok ? 'healthy' : 'unhealthy',
32
+ latency_ms: latency,
33
+ circuit,
34
+ error: resp.ok ? undefined : `HTTP ${resp.status}`,
35
+ checked_at: new Date().toISOString(),
36
+ };
37
+ healthCache.set(provider.name, { result, ts: Date.now() });
38
+ return result;
39
+ }
40
+ catch (err) {
41
+ const result = {
42
+ provider: provider.name,
43
+ status: 'unhealthy',
44
+ latency_ms: Date.now() - start,
45
+ circuit,
46
+ error: err.message?.slice(0, 100) || 'unknown error',
47
+ checked_at: new Date().toISOString(),
48
+ };
49
+ healthCache.set(provider.name, { result, ts: Date.now() });
50
+ return result;
51
+ }
52
+ }
53
+ export async function checkAllProviders(config) {
54
+ const results = await Promise.allSettled(Object.entries(config.providers).map(async ([name, cfg]) => {
55
+ const apiKey = Bun.env[cfg.api_key_env || ''] || cfg.api_key || '';
56
+ if (!apiKey) {
57
+ return {
58
+ provider: name,
59
+ status: 'unknown',
60
+ latency_ms: 0,
61
+ circuit: circuitBreakerRegistry.getState(name),
62
+ error: 'no_api_key',
63
+ checked_at: new Date().toISOString(),
64
+ };
65
+ }
66
+ return checkProviderHealth({ name, base_url: cfg.base_url, api_key: apiKey });
67
+ }));
68
+ return results.map(r => {
69
+ if (r.status === 'fulfilled')
70
+ return r.value;
71
+ return {
72
+ provider: 'unknown',
73
+ status: 'unhealthy',
74
+ latency_ms: 0,
75
+ circuit: 'unknown',
76
+ error: r.reason?.message || 'check failed',
77
+ checked_at: new Date().toISOString(),
78
+ };
79
+ });
80
+ }
@@ -0,0 +1,4 @@
1
+ import type { ChatCompletionRequest, ResolvedProvider } from './types.js';
2
+ export declare function callChatCompletions(provider: ResolvedProvider, request: ChatCompletionRequest): Promise<Response>;
3
+ export declare function callResponsesApi(provider: ResolvedProvider, body: Record<string, any>): Promise<Response>;
4
+ export declare function buildStreamingResponse(upstreamResp: Response): Response;
@@ -0,0 +1,113 @@
1
+ import { circuitBreakerRegistry } from './circuit-breaker.js';
2
+ import { withRetry, isRetryable } from './retry.js';
3
+ // 所有目标 Provider 都兼容 OpenAI API 格式,统一客户端即可
4
+ export async function callChatCompletions(provider, request) {
5
+ const { base_url, api_key, name: providerName } = provider;
6
+ const { model, messages, stream, temperature, max_tokens, tools, tool_choice } = request;
7
+ const body = { model, messages };
8
+ if (stream !== undefined)
9
+ body.stream = stream;
10
+ if (temperature !== undefined)
11
+ body.temperature = temperature;
12
+ if (max_tokens !== undefined)
13
+ body.max_tokens = max_tokens;
14
+ if (tools)
15
+ body.tools = tools;
16
+ if (tool_choice)
17
+ body.tool_choice = tool_choice;
18
+ const headers = {
19
+ 'Content-Type': 'application/json',
20
+ Authorization: `Bearer ${api_key}`,
21
+ };
22
+ // OpenRouter 需要额外的头部
23
+ if (providerName === 'openrouter') {
24
+ headers['HTTP-Referer'] = 'http://127.0.0.1:3000';
25
+ headers['X-Title'] = 'Agent Mesh Gateway';
26
+ }
27
+ const url = `${base_url.replace(/\/$/, '')}/chat/completions`;
28
+ // 熔断器检查
29
+ if (!circuitBreakerRegistry.canRequest(providerName)) {
30
+ throw new Error(`Circuit breaker open for ${providerName}`);
31
+ }
32
+ try {
33
+ const resp = await withRetry(providerName, async () => {
34
+ const r = await fetch(url, {
35
+ method: 'POST',
36
+ headers,
37
+ body: JSON.stringify(body),
38
+ signal: AbortSignal.timeout(120_000),
39
+ });
40
+ return r;
41
+ }, (attempt, status, delayMs) => {
42
+ console.warn(`[Retry] ${providerName} attempt ${attempt} after ${status} — retrying in ${delayMs}ms`);
43
+ });
44
+ if (!resp.ok && isRetryable(resp.status)) {
45
+ // Retry logic already handled in withRetry, but if we get here after max retries:
46
+ circuitBreakerRegistry.recordFailure(providerName);
47
+ }
48
+ else if (resp.ok) {
49
+ circuitBreakerRegistry.recordSuccess(providerName);
50
+ }
51
+ else {
52
+ circuitBreakerRegistry.recordFailure(providerName);
53
+ }
54
+ return resp;
55
+ }
56
+ catch (err) {
57
+ circuitBreakerRegistry.recordFailure(providerName);
58
+ throw err;
59
+ }
60
+ }
61
+ export async function callResponsesApi(provider, body) {
62
+ // Responses API → Chat Completions 转换
63
+ const messages = convertResponsesInputToMessages(body.input || []);
64
+ if (body.instructions) {
65
+ messages.unshift({ role: 'system', content: body.instructions });
66
+ }
67
+ return callChatCompletions(provider, {
68
+ model: body.model,
69
+ messages,
70
+ stream: body.stream,
71
+ tools: body.tools,
72
+ });
73
+ }
74
+ function convertResponsesInputToMessages(input) {
75
+ const messages = [];
76
+ for (const item of input) {
77
+ if (item.role === 'system') {
78
+ messages.push({ role: 'system', content: extractTextContent(item.content) });
79
+ }
80
+ else if (item.role === 'user') {
81
+ messages.push({ role: 'user', content: extractTextContent(item.content) });
82
+ }
83
+ else if (item.role === 'assistant') {
84
+ messages.push({ role: 'assistant', content: extractTextContent(item.content) });
85
+ }
86
+ else if (item.type === 'message') {
87
+ const role = item.role || 'user';
88
+ messages.push({ role, content: extractTextContent(item.content) });
89
+ }
90
+ }
91
+ return messages;
92
+ }
93
+ function extractTextContent(content) {
94
+ if (typeof content === 'string')
95
+ return content;
96
+ if (Array.isArray(content)) {
97
+ return content
98
+ .filter((p) => p.type === 'input_text' || p.type === 'output_text')
99
+ .map((p) => p.text || '')
100
+ .join('\n');
101
+ }
102
+ return String(content || '');
103
+ }
104
+ export function buildStreamingResponse(upstreamResp) {
105
+ return new Response(upstreamResp.body, {
106
+ status: upstreamResp.status,
107
+ headers: {
108
+ 'Content-Type': 'text/event-stream',
109
+ 'Cache-Control': 'no-cache',
110
+ Connection: 'keep-alive',
111
+ },
112
+ });
113
+ }
@@ -0,0 +1,4 @@
1
+ import type { QuotaInfo } from './types.js';
2
+ export declare function probeQuota(): Promise<Map<string, QuotaInfo>>;
3
+ export declare function isProviderAvailable(provider: string): boolean;
4
+ export declare function getQuotaSummary(): Record<string, QuotaInfo>;
@@ -0,0 +1,107 @@
1
+ let quotaCache = new Map();
2
+ let lastProbeTime = 0;
3
+ const QUOTA_TTL = 60_000; // 60秒缓存
4
+ export async function probeQuota() {
5
+ const now = Date.now();
6
+ if (now - lastProbeTime < QUOTA_TTL && quotaCache.size > 0) {
7
+ return quotaCache;
8
+ }
9
+ try {
10
+ const proc = Bun.spawn(['codexbar', 'usage', '--format', 'json', '--provider', 'all'], { stdout: 'pipe', stderr: 'pipe' });
11
+ const output = await new Response(proc.stdout).text();
12
+ if (output.trim()) {
13
+ const entries = JSON.parse(output);
14
+ quotaCache = new Map();
15
+ for (const entry of entries) {
16
+ const provider = entry.provider;
17
+ if (!provider)
18
+ continue;
19
+ const info = parseQuota(provider, entry);
20
+ quotaCache.set(provider, info);
21
+ }
22
+ lastProbeTime = now;
23
+ console.log(`[Quota] Refreshed: ${quotaCache.size} providers`);
24
+ }
25
+ }
26
+ catch (err) {
27
+ console.warn('[Quota] Probe failed, using stale cache:', err.message);
28
+ }
29
+ return quotaCache;
30
+ }
31
+ function parseQuota(provider, entry) {
32
+ let available = true;
33
+ let usedPercent;
34
+ let balance;
35
+ let summary = '';
36
+ try {
37
+ switch (provider) {
38
+ case 'codex': {
39
+ const credits = entry.credits;
40
+ const remaining = credits?.remaining ?? 0;
41
+ const secUsed = entry.usage?.secondary?.usedPercent ?? 0;
42
+ available = remaining > 0 || secUsed < 100;
43
+ usedPercent = secUsed;
44
+ summary = `Credits: ${remaining}, Secondary: ${secUsed}%`;
45
+ break;
46
+ }
47
+ case 'openai': {
48
+ // 直接 API Key 方式,总是 available(除非有 error)
49
+ available = !entry.error;
50
+ summary = entry.error ? `Error: ${entry.error.message}` : 'API Key configured';
51
+ break;
52
+ }
53
+ case 'deepseek': {
54
+ const desc = entry.usage?.primary?.resetDescription ?? '';
55
+ const match = desc.match(/¥([\d.]+)/);
56
+ balance = match ? parseFloat(match[1]) : -1;
57
+ available = (balance ?? -1) > 0;
58
+ summary = `Balance: ¥${(balance ?? -1).toFixed(2)}`;
59
+ break;
60
+ }
61
+ case 'openrouter': {
62
+ const orUsage = entry.usage?.openRouterUsage ?? {};
63
+ balance = orUsage.balance ?? 0;
64
+ usedPercent = orUsage.usedPercent ?? 0;
65
+ available = (balance ?? 0) > 0;
66
+ summary = `Balance: $${(balance ?? 0).toFixed(2)}, Used: ${usedPercent ?? 0}%`;
67
+ break;
68
+ }
69
+ case 'gemini': {
70
+ usedPercent = entry.usage?.primary?.usedPercent ?? 0;
71
+ available = (usedPercent ?? 100) < 95;
72
+ summary = `Used: ${usedPercent ?? 0}%`;
73
+ break;
74
+ }
75
+ case 'copilot': {
76
+ usedPercent = entry.usage?.primary?.usedPercent ?? 0;
77
+ available = (usedPercent ?? 0) < 100;
78
+ summary = `Used: ${usedPercent ?? 0}%`;
79
+ break;
80
+ }
81
+ case 'cursor': {
82
+ usedPercent = entry.usage?.primary?.usedPercent ?? 0;
83
+ available = (usedPercent ?? 0) < 100;
84
+ summary = `Used: ${usedPercent ?? 0}%`;
85
+ break;
86
+ }
87
+ case 'ollama':
88
+ available = true;
89
+ summary = 'Local - always available';
90
+ break;
91
+ default:
92
+ available = !entry.error;
93
+ summary = entry.error ? `Error: ${entry.error.message}` : 'Status unknown';
94
+ }
95
+ }
96
+ catch {
97
+ available = true; // 解析失败时假设可用
98
+ summary = 'Parse error, assuming available';
99
+ }
100
+ return { provider, available, usedPercent, balance, summary };
101
+ }
102
+ export function isProviderAvailable(provider) {
103
+ return quotaCache.get(provider)?.available ?? true;
104
+ }
105
+ export function getQuotaSummary() {
106
+ return Object.fromEntries(quotaCache);
107
+ }
@@ -0,0 +1,12 @@
1
+ interface LimitConfig {
2
+ rpm: number;
3
+ enabled: boolean;
4
+ }
5
+ export declare function initRateLimiter(configsOverride?: Record<string, Partial<LimitConfig>>): void;
6
+ export declare function checkRateLimit(path: string, ip: string): {
7
+ allowed: boolean;
8
+ limit: number;
9
+ remaining: number;
10
+ resetSeconds: number;
11
+ };
12
+ export {};
@@ -0,0 +1,51 @@
1
+ const buckets = new Map();
2
+ const configs = new Map();
3
+ let cleanupTimer = null;
4
+ const DEFAULTS = {
5
+ '/v1/chat/completions': { rpm: 60, enabled: true },
6
+ '/v1/responses': { rpm: 30, enabled: true },
7
+ };
8
+ export function initRateLimiter(configsOverride) {
9
+ for (const [path, defaults] of Object.entries(DEFAULTS)) {
10
+ configs.set(path, { ...defaults, ...configsOverride?.[path] });
11
+ }
12
+ // 每 60 秒清理过期桶
13
+ if (!cleanupTimer) {
14
+ cleanupTimer = setInterval(cleanupStaleBuckets, 60_000);
15
+ if (cleanupTimer.unref)
16
+ cleanupTimer.unref();
17
+ }
18
+ }
19
+ export function checkRateLimit(path, ip) {
20
+ const cfg = configs.get(path);
21
+ if (!cfg || !cfg.enabled) {
22
+ return { allowed: true, limit: 0, remaining: 0, resetSeconds: 0 };
23
+ }
24
+ const key = `${path}:${ip}`;
25
+ const now = Date.now();
26
+ let bucket = buckets.get(key);
27
+ if (!bucket) {
28
+ bucket = { tokens: cfg.rpm, lastRefill: now };
29
+ buckets.set(key, bucket);
30
+ }
31
+ const elapsed = (now - bucket.lastRefill) / 1000;
32
+ const refillRate = cfg.rpm / 60;
33
+ bucket.tokens = Math.min(cfg.rpm, bucket.tokens + elapsed * refillRate);
34
+ bucket.lastRefill = now;
35
+ // 重置时间 = 令牌桶回到满的时间
36
+ const tokensNeeded = cfg.rpm - bucket.tokens;
37
+ const resetSeconds = Math.ceil(tokensNeeded / refillRate);
38
+ if (bucket.tokens >= 1) {
39
+ bucket.tokens -= 1;
40
+ return { allowed: true, limit: cfg.rpm, remaining: Math.floor(bucket.tokens), resetSeconds };
41
+ }
42
+ return { allowed: false, limit: cfg.rpm, remaining: 0, resetSeconds: Math.max(1, resetSeconds) };
43
+ }
44
+ function cleanupStaleBuckets() {
45
+ const now = Date.now();
46
+ for (const [key, bucket] of buckets) {
47
+ if (now - bucket.lastRefill > 300_000) {
48
+ buckets.delete(key);
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,14 @@
1
+ export interface RetryConfig {
2
+ maxRetries: number;
3
+ baseDelayMs: number;
4
+ maxDelayMs: number;
5
+ retryableStatuses: number[];
6
+ }
7
+ export declare function configureRetry(config?: Partial<RetryConfig>): void;
8
+ export declare function getRetryConfig(): RetryConfig;
9
+ export declare function isRetryable(status: number): boolean;
10
+ export declare function getRetryDelay(attempt: number): number;
11
+ export declare function withRetry<T>(provider: string, fn: () => Promise<{
12
+ ok: boolean;
13
+ status: number;
14
+ } & T>, onRetry?: (attempt: number, status: number, delayMs: number) => void): Promise<T>;
@@ -0,0 +1,48 @@
1
+ const DEFAULTS = {
2
+ maxRetries: 3,
3
+ baseDelayMs: 500,
4
+ maxDelayMs: 10_000,
5
+ retryableStatuses: [429, 500, 502, 503, 504],
6
+ };
7
+ let globalConfig = { ...DEFAULTS };
8
+ export function configureRetry(config = {}) {
9
+ globalConfig = { ...DEFAULTS, ...config };
10
+ }
11
+ export function getRetryConfig() {
12
+ return globalConfig;
13
+ }
14
+ export function isRetryable(status) {
15
+ return globalConfig.retryableStatuses.includes(status);
16
+ }
17
+ export function getRetryDelay(attempt) {
18
+ const base = globalConfig.baseDelayMs * Math.pow(2, attempt);
19
+ const withJitter = base * (0.75 + Math.random() * 0.5);
20
+ return Math.min(withJitter, globalConfig.maxDelayMs);
21
+ }
22
+ export async function withRetry(provider, fn, onRetry) {
23
+ let lastError = null;
24
+ for (let attempt = 0; attempt <= globalConfig.maxRetries; attempt++) {
25
+ try {
26
+ const result = await fn();
27
+ if (!result.ok && isRetryable(result.status) && attempt < globalConfig.maxRetries) {
28
+ const delay = getRetryDelay(attempt);
29
+ onRetry?.(attempt + 1, result.status, delay);
30
+ await sleep(delay);
31
+ continue;
32
+ }
33
+ return result;
34
+ }
35
+ catch (err) {
36
+ lastError = err;
37
+ if (attempt < globalConfig.maxRetries) {
38
+ const delay = getRetryDelay(attempt);
39
+ onRetry?.(attempt + 1, 0, delay);
40
+ await sleep(delay);
41
+ }
42
+ }
43
+ }
44
+ throw lastError || new Error(`${provider}: max retries exhausted`);
45
+ }
46
+ function sleep(ms) {
47
+ return new Promise((resolve) => setTimeout(resolve, ms));
48
+ }
@@ -0,0 +1,4 @@
1
+ import type { ModelGatewayConfig, ResolvedProvider } from './types.js';
2
+ export declare function initModelRouter(cfg: ModelGatewayConfig): void;
3
+ export declare function getConfig(): ModelGatewayConfig;
4
+ export declare function resolveProvider(model: string): ResolvedProvider | null;
@@ -0,0 +1,79 @@
1
+ import { isProviderAvailable } from './quota.js';
2
+ import { circuitBreakerRegistry } from './circuit-breaker.js';
3
+ let config;
4
+ export function initModelRouter(cfg) {
5
+ config = cfg;
6
+ }
7
+ export function getConfig() {
8
+ return config;
9
+ }
10
+ // 解析 Provider,按优先级查找第一个可用的
11
+ export function resolveProvider(model) {
12
+ if (!config)
13
+ return null;
14
+ // 1. 按 model_routing 配置查找
15
+ const routingEntries = Object.entries(config.model_routing);
16
+ for (const [pattern, providers] of routingEntries) {
17
+ if (model.includes(pattern)) {
18
+ for (const providerName of providers) {
19
+ const providerCfg = config.providers[providerName];
20
+ if (!providerCfg)
21
+ continue;
22
+ const apiKey = resolveApiKey(providerName, providerCfg);
23
+ if (!apiKey)
24
+ continue;
25
+ // 熔断器检查:跳过 OPEN 状态的 Provider
26
+ if (circuitBreakerRegistry.isOpen(providerName))
27
+ continue;
28
+ // codex 和 openai 特殊处理:检查 Codex Plus 配额
29
+ if (providerName === 'openai') {
30
+ const codexAvailable = isProviderAvailable('codex');
31
+ if (!codexAvailable)
32
+ continue; // Codex Plus 配额耗尽,跳过
33
+ }
34
+ return {
35
+ name: providerName,
36
+ base_url: providerCfg.base_url,
37
+ api_key: apiKey,
38
+ };
39
+ }
40
+ break;
41
+ }
42
+ }
43
+ // 2. 全局 fallback 链
44
+ for (const providerName of config.fallback_chain) {
45
+ const providerCfg = config.providers[providerName];
46
+ if (!providerCfg)
47
+ continue;
48
+ const apiKey = resolveApiKey(providerName, providerCfg);
49
+ if (!apiKey)
50
+ continue;
51
+ // 熔断器检查:跳过 OPEN 状态的 Provider
52
+ if (circuitBreakerRegistry.isOpen(providerName))
53
+ continue;
54
+ return {
55
+ name: providerName,
56
+ base_url: providerCfg.base_url,
57
+ api_key: apiKey,
58
+ };
59
+ }
60
+ // 3. 终极兜底:第一个有 API Key 的 Provider(也检查熔断器)
61
+ for (const [name, cfg] of Object.entries(config.providers)) {
62
+ if (circuitBreakerRegistry.isOpen(name))
63
+ continue;
64
+ const key = resolveApiKey(name, cfg);
65
+ if (key) {
66
+ return { name, base_url: cfg.base_url, api_key: key };
67
+ }
68
+ }
69
+ return null;
70
+ }
71
+ function resolveApiKey(_name, providerCfg) {
72
+ if (providerCfg.api_key && providerCfg.api_key !== '') {
73
+ return providerCfg.api_key;
74
+ }
75
+ if (providerCfg.api_key_env) {
76
+ return Bun.env[providerCfg.api_key_env] || null;
77
+ }
78
+ return null;
79
+ }
@@ -0,0 +1,2 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ export declare function modelGatewayRoutes(fastify: FastifyInstance): Promise<void>;