kiro-proxy 0.1.17 → 0.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.
- package/README.md +54 -15
- package/package.json +6 -3
- package/proxy-config.js +21 -0
- package/q-client.js +10 -2
- package/server.js +17 -1
- package/token-counter.js +126 -3
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。
|
|
@@ -77,14 +97,33 @@ Claude Code 默认使用 Anthropic 官方 model ID,需要通过环境变量映
|
|
|
77
97
|
"env": {
|
|
78
98
|
"ANTHROPIC_AUTH_TOKEN": "any",
|
|
79
99
|
"ANTHROPIC_BASE_URL": "http://localhost:3456",
|
|
80
|
-
"ANTHROPIC_MODEL": "claude-sonnet-4.6",
|
|
81
100
|
"ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-sonnet-4.6",
|
|
82
101
|
"ANTHROPIC_DEFAULT_OPUS_MODEL": "claude-opus-4.6",
|
|
83
102
|
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "claude-haiku-4.5"
|
|
84
|
-
}
|
|
103
|
+
},
|
|
104
|
+
"model": "sonnet"
|
|
85
105
|
}
|
|
86
106
|
```
|
|
87
107
|
|
|
108
|
+
`model` 可选值:`sonnet`、`opus`、`haiku`,添加 `[1m]` 后缀可启用 1M 上下文窗口(如 `"opus[1m]"`)。
|
|
109
|
+
|
|
110
|
+
> 注意:不要设置 `ANTHROPIC_MODEL` 环境变量,它会覆盖 `model` 字段,导致上下文窗口等配置失效。
|
|
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
|
+
|
|
88
127
|
## 相关项目
|
|
89
128
|
|
|
90
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.
|
|
3
|
+
"version": "0.2.0",
|
|
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
|
}
|
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}`);
|
package/token-counter.js
CHANGED
|
@@ -1,11 +1,131 @@
|
|
|
1
|
+
const CLAUDE_MULTIPLIERS = {
|
|
2
|
+
word: 1.13,
|
|
3
|
+
number: 1.63,
|
|
4
|
+
cjk: 1.21,
|
|
5
|
+
symbol: 0.4,
|
|
6
|
+
mathSymbol: 4.52,
|
|
7
|
+
urlDelim: 1.26,
|
|
8
|
+
atSign: 2.82,
|
|
9
|
+
emoji: 2.6,
|
|
10
|
+
newline: 0.89,
|
|
11
|
+
space: 0.39,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function isCJK(code) {
|
|
15
|
+
return (
|
|
16
|
+
(code >= 0x4e00 && code <= 0x9fff) ||
|
|
17
|
+
(code >= 0x3400 && code <= 0x4dbf) ||
|
|
18
|
+
(code >= 0x20000 && code <= 0x2a6df) ||
|
|
19
|
+
(code >= 0x2a700 && code <= 0x2b73f) ||
|
|
20
|
+
(code >= 0x2b740 && code <= 0x2b81f) ||
|
|
21
|
+
(code >= 0x3040 && code <= 0x30ff) ||
|
|
22
|
+
(code >= 0xac00 && code <= 0xd7a3)
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isEmoji(code) {
|
|
27
|
+
return (
|
|
28
|
+
(code >= 0x1f300 && code <= 0x1f9ff) ||
|
|
29
|
+
(code >= 0x2600 && code <= 0x26ff) ||
|
|
30
|
+
(code >= 0x2700 && code <= 0x27bf) ||
|
|
31
|
+
(code >= 0x1f600 && code <= 0x1f64f) ||
|
|
32
|
+
(code >= 0x1f900 && code <= 0x1f9ff) ||
|
|
33
|
+
(code >= 0x1fa00 && code <= 0x1faff)
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isMathSymbol(code) {
|
|
38
|
+
if (code >= 0x2200 && code <= 0x22ff) return true;
|
|
39
|
+
if (code >= 0x2a00 && code <= 0x2aff) return true;
|
|
40
|
+
if (code >= 0x1d400 && code <= 0x1d7ff) return true;
|
|
41
|
+
const mathChars =
|
|
42
|
+
'∑∫∂√∞≤≥≠≈±×÷∈∉∋∌⊂⊃⊆⊇∪∩∧∨¬∀∃∄∅∆∇∝∟∠∡∢°′″‴⁺⁻⁼⁽⁾ⁿ₀₁₂₃₄₅₆₇₈₉₊₋₌₍₎²³¹⁴⁵⁶⁷⁸⁹⁰';
|
|
43
|
+
return mathChars.includes(String.fromCodePoint(code));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isURLDelim(code) {
|
|
47
|
+
return '/:?&=;#%'.includes(String.fromCodePoint(code));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isLetterOrDigit(code) {
|
|
51
|
+
return (
|
|
52
|
+
(code >= 0x41 && code <= 0x5a) ||
|
|
53
|
+
(code >= 0x61 && code <= 0x7a) ||
|
|
54
|
+
(code >= 0x30 && code <= 0x39) ||
|
|
55
|
+
(code >= 0xc0 && code <= 0x24f)
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isDigit(code) {
|
|
60
|
+
return code >= 0x30 && code <= 0x39;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const NONE = 0;
|
|
64
|
+
const LATIN = 1;
|
|
65
|
+
const NUMBER = 2;
|
|
66
|
+
|
|
1
67
|
function countText(text) {
|
|
2
68
|
if (!text) return 0;
|
|
3
|
-
|
|
69
|
+
|
|
70
|
+
const m = CLAUDE_MULTIPLIERS;
|
|
71
|
+
let count = 0;
|
|
72
|
+
let currentWordType = NONE;
|
|
73
|
+
|
|
74
|
+
for (const char of text) {
|
|
75
|
+
const code = char.codePointAt(0);
|
|
76
|
+
|
|
77
|
+
if (char === ' ' || char === '\t' || char === '\n' || char === '\r') {
|
|
78
|
+
currentWordType = NONE;
|
|
79
|
+
if (char === '\n' || char === '\t') {
|
|
80
|
+
count += m.newline;
|
|
81
|
+
} else {
|
|
82
|
+
count += m.space;
|
|
83
|
+
}
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (isCJK(code)) {
|
|
88
|
+
currentWordType = NONE;
|
|
89
|
+
count += m.cjk;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (isEmoji(code)) {
|
|
94
|
+
currentWordType = NONE;
|
|
95
|
+
count += m.emoji;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (isLetterOrDigit(code)) {
|
|
100
|
+
const isNum = isDigit(code);
|
|
101
|
+
const newType = isNum ? NUMBER : LATIN;
|
|
102
|
+
|
|
103
|
+
if (currentWordType === NONE || currentWordType !== newType) {
|
|
104
|
+
count += isNum ? m.number : m.word;
|
|
105
|
+
currentWordType = newType;
|
|
106
|
+
}
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
currentWordType = NONE;
|
|
111
|
+
if (isMathSymbol(code)) {
|
|
112
|
+
count += m.mathSymbol;
|
|
113
|
+
} else if (code === 0x40) {
|
|
114
|
+
count += m.atSign;
|
|
115
|
+
} else if (isURLDelim(code)) {
|
|
116
|
+
count += m.urlDelim;
|
|
117
|
+
} else {
|
|
118
|
+
count += m.symbol;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return Math.ceil(count);
|
|
4
123
|
}
|
|
5
124
|
|
|
6
125
|
export function countMessages(messages, system) {
|
|
7
126
|
let tokens = 0;
|
|
8
|
-
if (system)
|
|
127
|
+
if (system)
|
|
128
|
+
tokens += countText(typeof system === 'string' ? system : JSON.stringify(system));
|
|
9
129
|
for (const msg of messages || []) {
|
|
10
130
|
tokens += 4;
|
|
11
131
|
if (typeof msg.content === 'string') {
|
|
@@ -13,7 +133,10 @@ export function countMessages(messages, system) {
|
|
|
13
133
|
} else if (Array.isArray(msg.content)) {
|
|
14
134
|
for (const block of msg.content) {
|
|
15
135
|
if (block.type === 'text') tokens += countText(block.text);
|
|
16
|
-
else if (block.type === 'tool_result')
|
|
136
|
+
else if (block.type === 'tool_result')
|
|
137
|
+
tokens += countText(
|
|
138
|
+
typeof block.content === 'string' ? block.content : JSON.stringify(block.content),
|
|
139
|
+
);
|
|
17
140
|
else if (block.type === 'tool_use') tokens += countText(JSON.stringify(block.input));
|
|
18
141
|
}
|
|
19
142
|
}
|