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 CHANGED
@@ -13,7 +13,7 @@
13
13
  ## 快速开始
14
14
 
15
15
  ```bash
16
- npx @colin3191/kiro-proxy
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/chat/completionsOpenAI 兼容
37
+ ### POST /v1/messagesAnthropic 兼容
36
38
 
37
39
  ```bash
38
40
  # 非流式
39
- curl http://localhost:3456/v1/chat/completions \
41
+ curl http://localhost:3456/v1/messages \
40
42
  -H "Content-Type: application/json" \
41
- -d '{"model": "claude-sonnet-4.6", "messages": [{"role": "user", "content": "Hello"}]}'
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/chat/completions \
47
+ curl http://localhost:3456/v1/messages \
45
48
  -H "Content-Type: application/json" \
46
- -d '{"model": "claude-sonnet-4.6", "messages": [{"role": "user", "content": "Hello"}], "stream": true}'
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/messagesAnthropic 兼容
53
+ ### POST /v1/chat/completionsOpenAI 兼容
50
54
 
51
55
  ```bash
52
56
  # 非流式
53
- curl http://localhost:3456/v1/messages \
57
+ curl http://localhost:3456/v1/chat/completions \
54
58
  -H "Content-Type: application/json" \
55
- -H "x-api-key: any" \
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/messages \
62
+ curl http://localhost:3456/v1/chat/completions \
60
63
  -H "Content-Type: application/json" \
61
- -H "x-api-key: any" \
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.18",
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
  }
@@ -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 client = new CodeWhispererStreaming({
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 为空,结果放在 userInputMessageContext.toolResults
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
- // 如果末尾是 assistant(不应该发生),追加空 user
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}`);