kiro-proxy 0.1.18 → 0.2.1
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/README.md +48 -13
- package/package.json +6 -3
- package/proxy-config.js +21 -0
- package/q-client.js +36 -5
- package/server.js +17 -1
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
## 快速开始
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
npx
|
|
16
|
+
npx kiro-proxy
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
服务默认监听 `http://localhost:3456`。
|
|
@@ -23,6 +23,8 @@ npx @colin3191/kiro-proxy
|
|
|
23
23
|
| 环境变量 | 默认值 | 说明 |
|
|
24
24
|
|----------|--------|------|
|
|
25
25
|
| `PORT` | `3456` | 监听端口 |
|
|
26
|
+
| `PROXY_API_KEY` | 无 | 设置后所有请求需携带此 key 进行鉴权,未设置则不校验 |
|
|
27
|
+
| `HTTPS_PROXY` | 无 | HTTP/HTTPS 代理地址,如 `http://127.0.0.1:7890` |
|
|
26
28
|
|
|
27
29
|
## API
|
|
28
30
|
|
|
@@ -32,40 +34,58 @@ npx @colin3191/kiro-proxy
|
|
|
32
34
|
curl http://localhost:3456/v1/models
|
|
33
35
|
```
|
|
34
36
|
|
|
35
|
-
### POST /v1/
|
|
37
|
+
### POST /v1/messages — Anthropic 兼容
|
|
36
38
|
|
|
37
39
|
```bash
|
|
38
40
|
# 非流式
|
|
39
|
-
curl http://localhost:3456/v1/
|
|
41
|
+
curl http://localhost:3456/v1/messages \
|
|
40
42
|
-H "Content-Type: application/json" \
|
|
41
|
-
-
|
|
43
|
+
-H "x-api-key: any" \
|
|
44
|
+
-d '{"model": "claude-sonnet-4.6", "max_tokens": 1024, "messages": [{"role": "user", "content": "Hello"}]}'
|
|
42
45
|
|
|
43
46
|
# 流式
|
|
44
|
-
curl http://localhost:3456/v1/
|
|
47
|
+
curl http://localhost:3456/v1/messages \
|
|
45
48
|
-H "Content-Type: application/json" \
|
|
46
|
-
-
|
|
49
|
+
-H "x-api-key: any" \
|
|
50
|
+
-d '{"model": "claude-sonnet-4.6", "max_tokens": 1024, "messages": [{"role": "user", "content": "Hello"}], "stream": true}'
|
|
47
51
|
```
|
|
48
52
|
|
|
49
|
-
### POST /v1/
|
|
53
|
+
### POST /v1/chat/completions — OpenAI 兼容
|
|
50
54
|
|
|
51
55
|
```bash
|
|
52
56
|
# 非流式
|
|
53
|
-
curl http://localhost:3456/v1/
|
|
57
|
+
curl http://localhost:3456/v1/chat/completions \
|
|
54
58
|
-H "Content-Type: application/json" \
|
|
55
|
-
-
|
|
56
|
-
-d '{"model": "claude-sonnet-4.6", "max_tokens": 1024, "messages": [{"role": "user", "content": "Hello"}]}'
|
|
59
|
+
-d '{"model": "claude-sonnet-4.6", "messages": [{"role": "user", "content": "Hello"}]}'
|
|
57
60
|
|
|
58
61
|
# 流式
|
|
59
|
-
curl http://localhost:3456/v1/
|
|
62
|
+
curl http://localhost:3456/v1/chat/completions \
|
|
60
63
|
-H "Content-Type: application/json" \
|
|
61
|
-
-
|
|
62
|
-
-d '{"model": "claude-sonnet-4.6", "max_tokens": 1024, "messages": [{"role": "user", "content": "Hello"}], "stream": true}'
|
|
64
|
+
-d '{"model": "claude-sonnet-4.6", "messages": [{"role": "user", "content": "Hello"}], "stream": true}'
|
|
63
65
|
```
|
|
64
66
|
|
|
65
67
|
### GET /health
|
|
66
68
|
|
|
67
69
|
检查 token 状态及过期时间。
|
|
68
70
|
|
|
71
|
+
### GET /credits
|
|
72
|
+
|
|
73
|
+
查询积分消耗统计,支持 `period` 参数:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# 今日消耗(默认)
|
|
77
|
+
curl http://localhost:3456/credits
|
|
78
|
+
|
|
79
|
+
# 最近 7 天
|
|
80
|
+
curl http://localhost:3456/credits?period=7d
|
|
81
|
+
|
|
82
|
+
# 最近 30 天
|
|
83
|
+
curl http://localhost:3456/credits?period=30d
|
|
84
|
+
|
|
85
|
+
# 全部
|
|
86
|
+
curl http://localhost:3456/credits?period=all
|
|
87
|
+
```
|
|
88
|
+
|
|
69
89
|
## 与 Claude Code 集成
|
|
70
90
|
|
|
71
91
|
Claude Code 默认使用 Anthropic 官方 model ID,需要通过环境变量映射到 Q Developer 的 model ID。
|
|
@@ -89,6 +109,21 @@ Claude Code 默认使用 Anthropic 官方 model ID,需要通过环境变量映
|
|
|
89
109
|
|
|
90
110
|
> 注意:不要设置 `ANTHROPIC_MODEL` 环境变量,它会覆盖 `model` 字段,导致上下文窗口等配置失效。
|
|
91
111
|
|
|
112
|
+
## 代理设置
|
|
113
|
+
|
|
114
|
+
自 2026 年 5 月 1 日起,Kiro 上的 Claude 模型无法在中国大陆及港澳台地区使用。如果遇到 `Invalid model` 错误,请配置代理。
|
|
115
|
+
|
|
116
|
+
> 注意:代理节点需选择其他地区(如新加坡、泰国、韩国等)。
|
|
117
|
+
|
|
118
|
+
通过环境变量设置 HTTP 代理:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
# 设置代理后启动
|
|
122
|
+
HTTPS_PROXY=http://127.0.0.1:7890 npx kiro-proxy
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
支持的环境变量:`HTTPS_PROXY`、`https_proxy`、`HTTP_PROXY`、`http_proxy`,优先级从左到右。
|
|
126
|
+
|
|
92
127
|
## 相关项目
|
|
93
128
|
|
|
94
129
|
- [kiro-web-search](https://github.com/Colin3191/kiro-web-search) — 将 Kiro 内置的联网搜索封装为 MCP server,可在 Claude Code 等客户端中使用
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kiro-proxy",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Kiro API proxy with OpenAI and Anthropic compatible endpoints",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"token-reader.js",
|
|
16
16
|
"token-counter.js",
|
|
17
17
|
"usage-tracker.js",
|
|
18
|
-
"logger.js"
|
|
18
|
+
"logger.js",
|
|
19
|
+
"proxy-config.js"
|
|
19
20
|
],
|
|
20
21
|
"repository": {
|
|
21
22
|
"type": "git",
|
|
@@ -23,6 +24,8 @@
|
|
|
23
24
|
},
|
|
24
25
|
"dependencies": {
|
|
25
26
|
"@aws/codewhisperer-streaming-client": "^1.0.34",
|
|
26
|
-
"express": "^4.21.0"
|
|
27
|
+
"express": "^4.21.0",
|
|
28
|
+
"https-proxy-agent": "^7.0.0",
|
|
29
|
+
"undici": "^6.19.0"
|
|
27
30
|
}
|
|
28
31
|
}
|
package/proxy-config.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { setGlobalDispatcher, EnvHttpProxyAgent } from 'undici';
|
|
2
|
+
import { HttpsProxyAgent } from 'https-proxy-agent';
|
|
3
|
+
|
|
4
|
+
export function getProxyUrl() {
|
|
5
|
+
return process.env.HTTPS_PROXY || process.env.https_proxy ||
|
|
6
|
+
process.env.HTTP_PROXY || process.env.http_proxy || '';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function initGlobalProxy() {
|
|
10
|
+
const proxyUrl = getProxyUrl();
|
|
11
|
+
if (proxyUrl) {
|
|
12
|
+
setGlobalDispatcher(new EnvHttpProxyAgent());
|
|
13
|
+
}
|
|
14
|
+
return proxyUrl;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createProxyAgent() {
|
|
18
|
+
const proxyUrl = getProxyUrl();
|
|
19
|
+
if (!proxyUrl) return undefined;
|
|
20
|
+
return new HttpsProxyAgent(proxyUrl);
|
|
21
|
+
}
|
package/q-client.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CodeWhispererStreaming, GenerateAssistantResponseCommand } from '@aws/codewhisperer-streaming-client';
|
|
2
2
|
import crypto from 'crypto';
|
|
3
3
|
import os from 'os';
|
|
4
|
+
import { createProxyAgent } from './proxy-config.js';
|
|
4
5
|
|
|
5
6
|
// region → endpoint 映射
|
|
6
7
|
const REGION_ENDPOINTS = {
|
|
@@ -71,12 +72,19 @@ export function createClient(accessToken, { endpoint, region, authMethod, profil
|
|
|
71
72
|
const finalRegion = region || arnRegion || DEFAULT_REGION;
|
|
72
73
|
const finalEndpoint = endpoint || endpointForRegion(finalRegion);
|
|
73
74
|
|
|
74
|
-
const
|
|
75
|
+
const clientConfig = {
|
|
75
76
|
region: finalRegion,
|
|
76
77
|
endpoint: finalEndpoint,
|
|
77
78
|
token: { token: accessToken },
|
|
78
79
|
customUserAgent: buildUserAgent(machineId),
|
|
79
|
-
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const proxyAgent = createProxyAgent();
|
|
83
|
+
if (proxyAgent) {
|
|
84
|
+
clientConfig.requestHandler = { httpsAgent: proxyAgent };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const client = new CodeWhispererStreaming(clientConfig);
|
|
80
88
|
addRequiredHeaders(client, { authMethod, provider });
|
|
81
89
|
return client;
|
|
82
90
|
}
|
|
@@ -157,6 +165,25 @@ function extractText(content) {
|
|
|
157
165
|
.join('');
|
|
158
166
|
}
|
|
159
167
|
|
|
168
|
+
/**
|
|
169
|
+
* 从 assistant content blocks 中提取 thinking → CodeWhisperer reasoningContent
|
|
170
|
+
* Anthropic 格式: { type: "thinking", thinking: "...", signature: "..." }
|
|
171
|
+
* CodeWhisperer 格式: { reasoningText: { text, signature? } }
|
|
172
|
+
*/
|
|
173
|
+
function extractReasoning(content) {
|
|
174
|
+
if (!Array.isArray(content)) return undefined;
|
|
175
|
+
const thinkingBlocks = content.filter(b => b.type === 'thinking' && typeof b.thinking === 'string' && b.thinking.length > 0);
|
|
176
|
+
if (thinkingBlocks.length === 0) return undefined;
|
|
177
|
+
const text = thinkingBlocks.map(b => b.thinking).join('');
|
|
178
|
+
const sig = thinkingBlocks.map(b => b.signature).find(s => typeof s === 'string' && s.length > 0);
|
|
179
|
+
return {
|
|
180
|
+
reasoningText: {
|
|
181
|
+
text,
|
|
182
|
+
...(sig && { signature: sig }),
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
160
187
|
/**
|
|
161
188
|
* 从 assistant content blocks 中提取 tool_use 调用
|
|
162
189
|
*/
|
|
@@ -225,10 +252,12 @@ export function convertMessages(messages, { modelId, system, tools } = {}) {
|
|
|
225
252
|
const images = extractImages(msg.content);
|
|
226
253
|
|
|
227
254
|
if (toolResults.length > 0) {
|
|
228
|
-
// tool_result 消息:content
|
|
255
|
+
// tool_result 消息:text block 作为 content,tool_result 放在 userInputMessageContext
|
|
256
|
+
// 关键:Claude Code 的 ESC 中断会把 tool_result + [Request interrupted] + 新 prompt 打包成同一条 user message 的多个 content block,
|
|
257
|
+
// 如果这里把 content 写死成 '',中断标记和新 prompt 会被静默丢弃,模型无法感知中断
|
|
229
258
|
history.push({
|
|
230
259
|
userInputMessage: {
|
|
231
|
-
content:
|
|
260
|
+
content: text,
|
|
232
261
|
modelId: validModelId,
|
|
233
262
|
origin: 'AI_EDITOR',
|
|
234
263
|
userInputMessageContext: { toolResults },
|
|
@@ -246,10 +275,12 @@ export function convertMessages(messages, { modelId, system, tools } = {}) {
|
|
|
246
275
|
} else if (msg.role === 'assistant') {
|
|
247
276
|
const text = extractText(msg.content);
|
|
248
277
|
const toolUses = extractToolUses(msg.content);
|
|
278
|
+
const reasoningContent = extractReasoning(msg.content);
|
|
249
279
|
history.push({
|
|
250
280
|
assistantResponseMessage: {
|
|
251
281
|
content: text,
|
|
252
282
|
toolUses: toolUses.length > 0 ? toolUses : undefined,
|
|
283
|
+
...(reasoningContent && { reasoningContent }),
|
|
253
284
|
},
|
|
254
285
|
});
|
|
255
286
|
}
|
|
@@ -257,7 +288,7 @@ export function convertMessages(messages, { modelId, system, tools } = {}) {
|
|
|
257
288
|
}
|
|
258
289
|
|
|
259
290
|
// 确保 history 以 user→assistant 交替,末尾是 user
|
|
260
|
-
//
|
|
291
|
+
// 桥接后末尾仍可能是 assistant;CW 要求 currentMessage 必须是 userInputMessage
|
|
261
292
|
const last = history.at(-1);
|
|
262
293
|
if (last?.assistantResponseMessage) {
|
|
263
294
|
history.push({
|
package/server.js
CHANGED
|
@@ -3,14 +3,29 @@ import express from 'express';
|
|
|
3
3
|
import crypto from 'crypto';
|
|
4
4
|
import { getAccessToken } from './token-reader.js';
|
|
5
5
|
import { createClient, chat, chatStream, listAvailableModels } from './q-client.js';
|
|
6
|
-
import { c, log, logSummary, reqId, tagError } from './logger.js';
|
|
6
|
+
import { c, log, tagLog, logSummary, reqId, tagError } from './logger.js';
|
|
7
7
|
import { countMessages, countContent } from './token-counter.js';
|
|
8
8
|
import { recordUsage, queryUsage, todaySummary } from './usage-tracker.js';
|
|
9
|
+
import { initGlobalProxy } from './proxy-config.js';
|
|
10
|
+
|
|
11
|
+
const proxyUrl = initGlobalProxy();
|
|
12
|
+
if (proxyUrl) tagLog('proxy', `Using proxy: ${proxyUrl}`);
|
|
9
13
|
|
|
10
14
|
const app = express();
|
|
11
15
|
app.use(express.json({ limit: '10mb' }));
|
|
12
16
|
|
|
13
17
|
const PORT = process.env.PORT || 3456;
|
|
18
|
+
const PROXY_API_KEY = process.env.PROXY_API_KEY;
|
|
19
|
+
|
|
20
|
+
function authMiddleware(req, res, next) {
|
|
21
|
+
if (!PROXY_API_KEY) return next();
|
|
22
|
+
const auth = req.headers['authorization'];
|
|
23
|
+
const token = auth?.startsWith('Bearer ') ? auth.slice(7) : null;
|
|
24
|
+
if (token === PROXY_API_KEY) return next();
|
|
25
|
+
res.status(401).json({ type: 'error', error: { type: 'authentication_error', message: 'Invalid or missing API key' } });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
app.use(authMiddleware);
|
|
14
29
|
|
|
15
30
|
let cachedClient = null;
|
|
16
31
|
let cachedToken = null;
|
|
@@ -357,6 +372,7 @@ app.listen(PORT, async () => {
|
|
|
357
372
|
console.log(` ${c.gray}OpenAI: ${c.reset} http://localhost:${PORT}/v1/chat/completions`);
|
|
358
373
|
console.log(` ${c.gray}Models: ${c.reset} http://localhost:${PORT}/v1/models`);
|
|
359
374
|
console.log(` ${c.gray}Credits: ${c.reset} http://localhost:${PORT}/credits`);
|
|
375
|
+
console.log(` ${c.gray}Auth: ${c.reset} ${PROXY_API_KEY ? `${c.green}enabled${c.reset} (PROXY_API_KEY)` : `${c.yellow}disabled${c.reset} (no PROXY_API_KEY set)`}`);
|
|
360
376
|
try {
|
|
361
377
|
const t = await getAccessToken();
|
|
362
378
|
console.log(` ${c.gray}Provider: ${c.yellow}${t.provider || 'unknown'}${c.reset}, Expires: ${c.dim}${t.expiresAt || 'unknown'}${c.reset}`);
|