llmapi-v2 2.1.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.
- package/.env.example +40 -0
- package/Dockerfile +17 -0
- package/dist/config.d.ts +48 -0
- package/dist/config.js +98 -0
- package/dist/config.js.map +1 -0
- package/dist/converter/request.d.ts +6 -0
- package/dist/converter/request.js +184 -0
- package/dist/converter/request.js.map +1 -0
- package/dist/converter/response.d.ts +6 -0
- package/dist/converter/response.js +76 -0
- package/dist/converter/response.js.map +1 -0
- package/dist/converter/stream.d.ts +54 -0
- package/dist/converter/stream.js +318 -0
- package/dist/converter/stream.js.map +1 -0
- package/dist/converter/types.d.ts +239 -0
- package/dist/converter/types.js +6 -0
- package/dist/converter/types.js.map +1 -0
- package/dist/data/posts.d.ts +19 -0
- package/dist/data/posts.js +462 -0
- package/dist/data/posts.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +233 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/api-key-auth.d.ts +6 -0
- package/dist/middleware/api-key-auth.js +76 -0
- package/dist/middleware/api-key-auth.js.map +1 -0
- package/dist/middleware/quota-guard.d.ts +10 -0
- package/dist/middleware/quota-guard.js +27 -0
- package/dist/middleware/quota-guard.js.map +1 -0
- package/dist/middleware/rate-limiter.d.ts +5 -0
- package/dist/middleware/rate-limiter.js +50 -0
- package/dist/middleware/rate-limiter.js.map +1 -0
- package/dist/middleware/request-logger.d.ts +6 -0
- package/dist/middleware/request-logger.js +37 -0
- package/dist/middleware/request-logger.js.map +1 -0
- package/dist/middleware/session-auth.d.ts +19 -0
- package/dist/middleware/session-auth.js +99 -0
- package/dist/middleware/session-auth.js.map +1 -0
- package/dist/providers/aliyun.d.ts +13 -0
- package/dist/providers/aliyun.js +20 -0
- package/dist/providers/aliyun.js.map +1 -0
- package/dist/providers/base-provider.d.ts +36 -0
- package/dist/providers/base-provider.js +133 -0
- package/dist/providers/base-provider.js.map +1 -0
- package/dist/providers/deepseek.d.ts +11 -0
- package/dist/providers/deepseek.js +18 -0
- package/dist/providers/deepseek.js.map +1 -0
- package/dist/providers/registry.d.ts +18 -0
- package/dist/providers/registry.js +98 -0
- package/dist/providers/registry.js.map +1 -0
- package/dist/providers/types.d.ts +17 -0
- package/dist/providers/types.js +3 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/routes/admin.d.ts +1 -0
- package/dist/routes/admin.js +153 -0
- package/dist/routes/admin.js.map +1 -0
- package/dist/routes/auth.d.ts +2 -0
- package/dist/routes/auth.js +318 -0
- package/dist/routes/auth.js.map +1 -0
- package/dist/routes/blog.d.ts +1 -0
- package/dist/routes/blog.js +29 -0
- package/dist/routes/blog.js.map +1 -0
- package/dist/routes/dashboard.d.ts +1 -0
- package/dist/routes/dashboard.js +184 -0
- package/dist/routes/dashboard.js.map +1 -0
- package/dist/routes/messages.d.ts +1 -0
- package/dist/routes/messages.js +309 -0
- package/dist/routes/messages.js.map +1 -0
- package/dist/routes/models.d.ts +1 -0
- package/dist/routes/models.js +39 -0
- package/dist/routes/models.js.map +1 -0
- package/dist/routes/payment.d.ts +1 -0
- package/dist/routes/payment.js +150 -0
- package/dist/routes/payment.js.map +1 -0
- package/dist/routes/sitemap.d.ts +1 -0
- package/dist/routes/sitemap.js +38 -0
- package/dist/routes/sitemap.js.map +1 -0
- package/dist/services/alipay.d.ts +27 -0
- package/dist/services/alipay.js +106 -0
- package/dist/services/alipay.js.map +1 -0
- package/dist/services/database.d.ts +4 -0
- package/dist/services/database.js +170 -0
- package/dist/services/database.js.map +1 -0
- package/dist/services/health-checker.d.ts +13 -0
- package/dist/services/health-checker.js +95 -0
- package/dist/services/health-checker.js.map +1 -0
- package/dist/services/mailer.d.ts +3 -0
- package/dist/services/mailer.js +91 -0
- package/dist/services/mailer.js.map +1 -0
- package/dist/services/metrics.d.ts +56 -0
- package/dist/services/metrics.js +94 -0
- package/dist/services/metrics.js.map +1 -0
- package/dist/services/remote-control.d.ts +20 -0
- package/dist/services/remote-control.js +209 -0
- package/dist/services/remote-control.js.map +1 -0
- package/dist/services/remote-ws.d.ts +5 -0
- package/dist/services/remote-ws.js +143 -0
- package/dist/services/remote-ws.js.map +1 -0
- package/dist/services/usage.d.ts +13 -0
- package/dist/services/usage.js +39 -0
- package/dist/services/usage.js.map +1 -0
- package/dist/utils/errors.d.ts +27 -0
- package/dist/utils/errors.js +48 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/logger.d.ts +2 -0
- package/dist/utils/logger.js +14 -0
- package/dist/utils/logger.js.map +1 -0
- package/docker-compose.yml +19 -0
- package/package.json +39 -0
- package/public/robots.txt +8 -0
- package/src/config.ts +140 -0
- package/src/converter/request.ts +207 -0
- package/src/converter/response.ts +85 -0
- package/src/converter/stream.ts +373 -0
- package/src/converter/types.ts +257 -0
- package/src/data/posts.ts +474 -0
- package/src/index.ts +219 -0
- package/src/middleware/api-key-auth.ts +82 -0
- package/src/middleware/quota-guard.ts +28 -0
- package/src/middleware/rate-limiter.ts +61 -0
- package/src/middleware/request-logger.ts +36 -0
- package/src/middleware/session-auth.ts +91 -0
- package/src/providers/aliyun.ts +16 -0
- package/src/providers/base-provider.ts +148 -0
- package/src/providers/deepseek.ts +14 -0
- package/src/providers/registry.ts +111 -0
- package/src/providers/types.ts +26 -0
- package/src/routes/admin.ts +169 -0
- package/src/routes/auth.ts +369 -0
- package/src/routes/blog.ts +28 -0
- package/src/routes/dashboard.ts +208 -0
- package/src/routes/messages.ts +346 -0
- package/src/routes/models.ts +37 -0
- package/src/routes/payment.ts +189 -0
- package/src/routes/sitemap.ts +40 -0
- package/src/services/alipay.ts +116 -0
- package/src/services/database.ts +187 -0
- package/src/services/health-checker.ts +115 -0
- package/src/services/mailer.ts +90 -0
- package/src/services/metrics.ts +104 -0
- package/src/services/remote-control.ts +226 -0
- package/src/services/remote-ws.ts +145 -0
- package/src/services/usage.ts +57 -0
- package/src/types/express.d.ts +46 -0
- package/src/utils/errors.ts +44 -0
- package/src/utils/logger.ts +8 -0
- package/tsconfig.json +17 -0
- package/views/pages/404.ejs +14 -0
- package/views/pages/admin.ejs +307 -0
- package/views/pages/blog-post.ejs +378 -0
- package/views/pages/blog.ejs +148 -0
- package/views/pages/dashboard.ejs +441 -0
- package/views/pages/docs.ejs +807 -0
- package/views/pages/index.ejs +416 -0
- package/views/pages/login.ejs +170 -0
- package/views/pages/orders.ejs +111 -0
- package/views/pages/pricing.ejs +379 -0
- package/views/pages/register.ejs +397 -0
- package/views/pages/remote.ejs +334 -0
- package/views/pages/settings.ejs +373 -0
- package/views/partials/header.ejs +70 -0
- package/views/partials/nav.ejs +140 -0
package/src/config.ts
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
export interface ProviderConfig {
|
|
2
|
+
name: string;
|
|
3
|
+
/** Native Anthropic-compatible endpoint (preferred) */
|
|
4
|
+
anthropicBaseUrl: string;
|
|
5
|
+
/** OpenAI-compatible endpoint (fallback for providers without native Anthropic support) */
|
|
6
|
+
openaiBaseUrl?: string;
|
|
7
|
+
apiKey: string;
|
|
8
|
+
enabled: boolean;
|
|
9
|
+
timeout: number;
|
|
10
|
+
/** Maps Claude model names to this provider's model names */
|
|
11
|
+
models: Record<string, string>;
|
|
12
|
+
/** Cost per 1M tokens in RMB: { input, output, cached_input } */
|
|
13
|
+
pricing: Record<string, { input: number; output: number; cached_input?: number }>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ModelRoute {
|
|
17
|
+
provider: string;
|
|
18
|
+
model: string;
|
|
19
|
+
priority: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Smart routing: which models to use based on task complexity */
|
|
23
|
+
export interface SmartRoutingConfig {
|
|
24
|
+
/** Model for heavy coding tasks (tools present, complex) */
|
|
25
|
+
codingModel: { provider: string; model: string };
|
|
26
|
+
/** Model for simple/chat tasks (no tools or trivial) */
|
|
27
|
+
lightModel: { provider: string; model: string };
|
|
28
|
+
/** Threshold: if message count < this AND no tools, use light model */
|
|
29
|
+
lightTaskMaxMessages: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface AppConfig {
|
|
33
|
+
port: number;
|
|
34
|
+
jwtSecret: string;
|
|
35
|
+
databaseUrl: string;
|
|
36
|
+
providers: ProviderConfig[];
|
|
37
|
+
modelRouting: Record<string, ModelRoute[]>;
|
|
38
|
+
smartRouting: SmartRoutingConfig;
|
|
39
|
+
siteUrl: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function env(key: string, fallback?: string): string {
|
|
43
|
+
const val = process.env[key];
|
|
44
|
+
if (val !== undefined) return val;
|
|
45
|
+
if (fallback !== undefined) return fallback;
|
|
46
|
+
throw new Error(`Missing required env var: ${key}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function envBool(key: string, fallback: boolean): boolean {
|
|
50
|
+
const val = process.env[key];
|
|
51
|
+
if (val === undefined) return fallback;
|
|
52
|
+
return val === 'true' || val === '1';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function envInt(key: string, fallback: number): number {
|
|
56
|
+
const val = process.env[key];
|
|
57
|
+
if (val === undefined) return fallback;
|
|
58
|
+
const n = parseInt(val, 10);
|
|
59
|
+
return isNaN(n) ? fallback : n;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function loadConfig(): AppConfig {
|
|
63
|
+
const providers: ProviderConfig[] = [];
|
|
64
|
+
|
|
65
|
+
// Alibaba Bailian (DashScope) - PRIMARY: native Anthropic endpoint (China domestic)
|
|
66
|
+
if (envBool('ALIYUN_ENABLED', false)) {
|
|
67
|
+
providers.push({
|
|
68
|
+
name: 'aliyun',
|
|
69
|
+
anthropicBaseUrl: env('ALIYUN_ANTHROPIC_URL', 'https://dashscope.aliyuncs.com/apps/anthropic'),
|
|
70
|
+
openaiBaseUrl: env('ALIYUN_OPENAI_URL', 'https://dashscope.aliyuncs.com/compatible-mode/v1'),
|
|
71
|
+
apiKey: env('ALIYUN_API_KEY', ''),
|
|
72
|
+
enabled: true,
|
|
73
|
+
timeout: 300_000,
|
|
74
|
+
models: {
|
|
75
|
+
'claude-sonnet-4-6': 'qwen3-coder-plus',
|
|
76
|
+
'claude-opus-4-6': 'qwen-max',
|
|
77
|
+
'claude-haiku-4-5': 'qwen-plus',
|
|
78
|
+
},
|
|
79
|
+
pricing: {
|
|
80
|
+
'qwen3-coder-plus': { input: 3.5, output: 7.0, cached_input: 0.35 },
|
|
81
|
+
'qwen-max': { input: 20.0, output: 60.0, cached_input: 2.0 },
|
|
82
|
+
'qwen-plus': { input: 0.5, output: 1.5, cached_input: 0.05 },
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// DeepSeek - native Anthropic endpoint for simple tasks
|
|
88
|
+
if (envBool('DEEPSEEK_ENABLED', false)) {
|
|
89
|
+
providers.push({
|
|
90
|
+
name: 'deepseek',
|
|
91
|
+
anthropicBaseUrl: env('DEEPSEEK_ANTHROPIC_URL', 'https://api.deepseek.com/anthropic'),
|
|
92
|
+
openaiBaseUrl: env('DEEPSEEK_OPENAI_URL', 'https://api.deepseek.com/v1'),
|
|
93
|
+
apiKey: env('DEEPSEEK_API_KEY', ''),
|
|
94
|
+
enabled: true,
|
|
95
|
+
timeout: 300_000,
|
|
96
|
+
models: {
|
|
97
|
+
'claude-sonnet-4-6': 'deepseek-chat',
|
|
98
|
+
'claude-opus-4-6': 'deepseek-chat',
|
|
99
|
+
'claude-haiku-4-5': 'deepseek-chat',
|
|
100
|
+
},
|
|
101
|
+
pricing: {
|
|
102
|
+
'deepseek-chat': { input: 1.0, output: 2.0, cached_input: 0.1 },
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Build model routing
|
|
108
|
+
const modelRouting: Record<string, ModelRoute[]> = {};
|
|
109
|
+
const claudeModels = ['claude-sonnet-4-6', 'claude-opus-4-6', 'claude-haiku-4-5'];
|
|
110
|
+
|
|
111
|
+
for (const claudeModel of claudeModels) {
|
|
112
|
+
const routes: ModelRoute[] = [];
|
|
113
|
+
for (let i = 0; i < providers.length; i++) {
|
|
114
|
+
const p = providers[i];
|
|
115
|
+
if (p.models[claudeModel]) {
|
|
116
|
+
routes.push({ provider: p.name, model: p.models[claudeModel], priority: i });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (routes.length > 0) {
|
|
120
|
+
modelRouting[claudeModel] = routes.sort((a, b) => a.priority - b.priority);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Smart routing config
|
|
125
|
+
const smartRouting: SmartRoutingConfig = {
|
|
126
|
+
codingModel: { provider: 'aliyun', model: 'qwen3-coder-plus' },
|
|
127
|
+
lightModel: { provider: 'deepseek', model: 'deepseek-chat' },
|
|
128
|
+
lightTaskMaxMessages: envInt('SMART_ROUTING_LIGHT_MAX_MESSAGES', 4),
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
port: envInt('PORT', 3000),
|
|
133
|
+
jwtSecret: env('JWT_SECRET', 'dev-secret-change-me'),
|
|
134
|
+
databaseUrl: env('DATABASE_URL', 'postgresql://llmapi_user:change-this@postgres:5432/llmapi'),
|
|
135
|
+
providers,
|
|
136
|
+
modelRouting,
|
|
137
|
+
smartRouting,
|
|
138
|
+
siteUrl: env('SITE_URL', 'http://localhost:3000'),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AnthropicRequest,
|
|
3
|
+
AnthropicMessage,
|
|
4
|
+
AnthropicContentBlock,
|
|
5
|
+
AnthropicToolDefinition,
|
|
6
|
+
AnthropicToolChoice,
|
|
7
|
+
OpenAIRequest,
|
|
8
|
+
OpenAIMessage,
|
|
9
|
+
OpenAITool,
|
|
10
|
+
OpenAIToolCall,
|
|
11
|
+
OpenAIContentPart,
|
|
12
|
+
} from './types';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Convert an Anthropic Messages API request to OpenAI Chat Completions format.
|
|
16
|
+
* This is the inbound conversion: Claude Code -> our proxy -> provider.
|
|
17
|
+
*/
|
|
18
|
+
export function convertRequest(req: AnthropicRequest): OpenAIRequest {
|
|
19
|
+
const openaiMessages = convertMessages(req.messages, req.system);
|
|
20
|
+
const openaiTools = convertTools(req.tools);
|
|
21
|
+
const openaiToolChoice = convertToolChoice(req.tool_choice);
|
|
22
|
+
|
|
23
|
+
const body: OpenAIRequest = {
|
|
24
|
+
model: '', // Will be set by the router
|
|
25
|
+
messages: openaiMessages,
|
|
26
|
+
stream: !!req.stream,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
if (req.max_tokens) body.max_tokens = req.max_tokens;
|
|
30
|
+
if (req.temperature !== undefined) body.temperature = req.temperature;
|
|
31
|
+
if (req.top_p !== undefined) body.top_p = req.top_p;
|
|
32
|
+
if (req.stop_sequences) body.stop = req.stop_sequences;
|
|
33
|
+
if (openaiTools) body.tools = openaiTools;
|
|
34
|
+
if (openaiToolChoice) body.tool_choice = openaiToolChoice;
|
|
35
|
+
if (req.stream) body.stream_options = { include_usage: true };
|
|
36
|
+
|
|
37
|
+
return body;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Convert Anthropic messages array + system prompt to OpenAI messages.
|
|
42
|
+
*/
|
|
43
|
+
function convertMessages(
|
|
44
|
+
messages: AnthropicMessage[],
|
|
45
|
+
system?: string | { type: string; text: string }[],
|
|
46
|
+
): OpenAIMessage[] {
|
|
47
|
+
const result: OpenAIMessage[] = [];
|
|
48
|
+
|
|
49
|
+
// System prompt
|
|
50
|
+
if (system) {
|
|
51
|
+
const systemText = typeof system === 'string'
|
|
52
|
+
? system
|
|
53
|
+
: system.map(b => b.text).join('\n');
|
|
54
|
+
if (systemText) {
|
|
55
|
+
result.push({ role: 'system', content: systemText });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (const msg of messages) {
|
|
60
|
+
if (msg.role === 'user') {
|
|
61
|
+
convertUserMessage(msg, result);
|
|
62
|
+
} else if (msg.role === 'assistant') {
|
|
63
|
+
convertAssistantMessage(msg, result);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Convert a user message. Anthropic user messages can contain interleaved
|
|
72
|
+
* text and tool_result blocks. OpenAI requires tool results as separate messages.
|
|
73
|
+
*/
|
|
74
|
+
function convertUserMessage(msg: AnthropicMessage, result: OpenAIMessage[]): void {
|
|
75
|
+
if (typeof msg.content === 'string') {
|
|
76
|
+
result.push({ role: 'user', content: msg.content });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const textParts: string[] = [];
|
|
81
|
+
const imageParts: OpenAIContentPart[] = [];
|
|
82
|
+
|
|
83
|
+
for (const block of msg.content) {
|
|
84
|
+
switch (block.type) {
|
|
85
|
+
case 'text':
|
|
86
|
+
textParts.push(block.text);
|
|
87
|
+
break;
|
|
88
|
+
|
|
89
|
+
case 'tool_result': {
|
|
90
|
+
// Each tool_result becomes a separate OpenAI tool message
|
|
91
|
+
let content: string;
|
|
92
|
+
if (typeof block.content === 'string') {
|
|
93
|
+
content = block.content;
|
|
94
|
+
} else if (Array.isArray(block.content)) {
|
|
95
|
+
content = block.content
|
|
96
|
+
.map(b => ('text' in b ? b.text : JSON.stringify(b)))
|
|
97
|
+
.join('\n');
|
|
98
|
+
} else {
|
|
99
|
+
content = '';
|
|
100
|
+
}
|
|
101
|
+
if (block.is_error) {
|
|
102
|
+
content = `[ERROR] ${content}`;
|
|
103
|
+
}
|
|
104
|
+
result.push({
|
|
105
|
+
role: 'tool',
|
|
106
|
+
tool_call_id: block.tool_use_id,
|
|
107
|
+
content,
|
|
108
|
+
});
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
case 'image': {
|
|
113
|
+
// Convert base64 image to OpenAI image_url format
|
|
114
|
+
const dataUrl = `data:${block.source.media_type};base64,${block.source.data}`;
|
|
115
|
+
imageParts.push({ type: 'image_url', image_url: { url: dataUrl } });
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Emit text/image as user message
|
|
122
|
+
if (textParts.length > 0 && imageParts.length === 0) {
|
|
123
|
+
result.push({ role: 'user', content: textParts.join('\n') });
|
|
124
|
+
} else if (imageParts.length > 0) {
|
|
125
|
+
const parts: OpenAIContentPart[] = [];
|
|
126
|
+
if (textParts.length > 0) {
|
|
127
|
+
parts.push({ type: 'text', text: textParts.join('\n') });
|
|
128
|
+
}
|
|
129
|
+
parts.push(...imageParts);
|
|
130
|
+
result.push({ role: 'user', content: parts });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Convert an assistant message. Anthropic assistant messages can contain
|
|
136
|
+
* text, thinking, and tool_use blocks. OpenAI uses tool_calls array.
|
|
137
|
+
*/
|
|
138
|
+
function convertAssistantMessage(msg: AnthropicMessage, result: OpenAIMessage[]): void {
|
|
139
|
+
if (typeof msg.content === 'string') {
|
|
140
|
+
result.push({ role: 'assistant', content: msg.content });
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const textParts: string[] = [];
|
|
145
|
+
const toolCalls: OpenAIToolCall[] = [];
|
|
146
|
+
|
|
147
|
+
for (const block of msg.content) {
|
|
148
|
+
switch (block.type) {
|
|
149
|
+
case 'text':
|
|
150
|
+
textParts.push(block.text);
|
|
151
|
+
break;
|
|
152
|
+
|
|
153
|
+
case 'thinking':
|
|
154
|
+
// Thinking blocks: some providers support reasoning_content,
|
|
155
|
+
// but for history replay we skip them (provider generates its own thinking)
|
|
156
|
+
break;
|
|
157
|
+
|
|
158
|
+
case 'tool_use':
|
|
159
|
+
toolCalls.push({
|
|
160
|
+
id: block.id,
|
|
161
|
+
type: 'function',
|
|
162
|
+
function: {
|
|
163
|
+
name: block.name,
|
|
164
|
+
arguments: JSON.stringify(block.input),
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const assistantMsg: any = { role: 'assistant' };
|
|
172
|
+
assistantMsg.content = textParts.length > 0 ? textParts.join('\n') : null;
|
|
173
|
+
if (toolCalls.length > 0) assistantMsg.tool_calls = toolCalls;
|
|
174
|
+
|
|
175
|
+
result.push(assistantMsg as OpenAIMessage);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Convert Anthropic tool definitions to OpenAI format.
|
|
180
|
+
*/
|
|
181
|
+
function convertTools(tools?: AnthropicToolDefinition[]): OpenAITool[] | undefined {
|
|
182
|
+
if (!tools || tools.length === 0) return undefined;
|
|
183
|
+
return tools.map(tool => ({
|
|
184
|
+
type: 'function' as const,
|
|
185
|
+
function: {
|
|
186
|
+
name: tool.name,
|
|
187
|
+
description: tool.description || '',
|
|
188
|
+
parameters: tool.input_schema || {},
|
|
189
|
+
},
|
|
190
|
+
}));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Convert Anthropic tool_choice to OpenAI format.
|
|
195
|
+
*/
|
|
196
|
+
function convertToolChoice(
|
|
197
|
+
choice?: AnthropicToolChoice,
|
|
198
|
+
): string | { type: 'function'; function: { name: string } } | undefined {
|
|
199
|
+
if (!choice) return undefined;
|
|
200
|
+
|
|
201
|
+
switch (choice.type) {
|
|
202
|
+
case 'auto': return 'auto';
|
|
203
|
+
case 'any': return 'required';
|
|
204
|
+
case 'tool': return { type: 'function', function: { name: choice.name } };
|
|
205
|
+
default: return 'auto';
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AnthropicResponse,
|
|
3
|
+
AnthropicResponseBlock,
|
|
4
|
+
OpenAIResponse,
|
|
5
|
+
} from './types';
|
|
6
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Convert an OpenAI Chat Completions response to Anthropic Messages format.
|
|
10
|
+
* Used for non-streaming requests only.
|
|
11
|
+
*/
|
|
12
|
+
export function convertResponse(
|
|
13
|
+
openaiResp: OpenAIResponse,
|
|
14
|
+
displayModel: string,
|
|
15
|
+
): AnthropicResponse {
|
|
16
|
+
const choice = openaiResp.choices?.[0];
|
|
17
|
+
if (!choice) {
|
|
18
|
+
return {
|
|
19
|
+
id: `msg_${uuidv4().replace(/-/g, '')}`,
|
|
20
|
+
type: 'message',
|
|
21
|
+
role: 'assistant',
|
|
22
|
+
model: displayModel,
|
|
23
|
+
content: [{ type: 'text', text: 'No response from backend.' }],
|
|
24
|
+
stop_reason: 'end_turn',
|
|
25
|
+
stop_sequence: null,
|
|
26
|
+
usage: { input_tokens: 0, output_tokens: 0 },
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const msg = choice.message;
|
|
31
|
+
const content: AnthropicResponseBlock[] = [];
|
|
32
|
+
|
|
33
|
+
// Reasoning / thinking content
|
|
34
|
+
if (msg.reasoning_content) {
|
|
35
|
+
content.push({ type: 'thinking', thinking: msg.reasoning_content });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Text content
|
|
39
|
+
if (msg.content) {
|
|
40
|
+
content.push({ type: 'text', text: msg.content });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Tool calls
|
|
44
|
+
if (msg.tool_calls && msg.tool_calls.length > 0) {
|
|
45
|
+
for (const tc of msg.tool_calls) {
|
|
46
|
+
let input: Record<string, unknown> = {};
|
|
47
|
+
try { input = JSON.parse(tc.function.arguments); } catch {}
|
|
48
|
+
content.push({
|
|
49
|
+
type: 'tool_use',
|
|
50
|
+
id: tc.id || `toolu_${uuidv4().replace(/-/g, '').slice(0, 24)}`,
|
|
51
|
+
name: tc.function.name,
|
|
52
|
+
input,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// If no content at all, add empty text
|
|
58
|
+
if (content.length === 0) {
|
|
59
|
+
content.push({ type: 'text', text: '' });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Determine stop reason
|
|
63
|
+
let stopReason: AnthropicResponse['stop_reason'] = 'end_turn';
|
|
64
|
+
if (msg.tool_calls && msg.tool_calls.length > 0) {
|
|
65
|
+
stopReason = 'tool_use';
|
|
66
|
+
} else if (choice.finish_reason === 'length') {
|
|
67
|
+
stopReason = 'max_tokens';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const usage = openaiResp.usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
id: `msg_${uuidv4().replace(/-/g, '')}`,
|
|
74
|
+
type: 'message',
|
|
75
|
+
role: 'assistant',
|
|
76
|
+
model: displayModel,
|
|
77
|
+
content,
|
|
78
|
+
stop_reason: stopReason,
|
|
79
|
+
stop_sequence: null,
|
|
80
|
+
usage: {
|
|
81
|
+
input_tokens: usage.prompt_tokens,
|
|
82
|
+
output_tokens: usage.completion_tokens,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|