aikev commited on
Commit
0731c95
·
1 Parent(s): b98a5d1
Files changed (6) hide show
  1. Dockerfile +32 -0
  2. app.py +305 -0
  3. docker-compose.yml +19 -0
  4. entrypoint.sh +41 -0
  5. requirements.txt +5 -0
  6. token_reader.py +77 -0
Dockerfile ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install system dependencies
6
+ RUN apt-get update && apt-get install -y \
7
+ gcc \
8
+ curl \
9
+ && rm -rf /var/lib/apt/lists/*
10
+
11
+ # Copy requirements and install Python dependencies
12
+ COPY requirements.txt .
13
+ RUN pip install --no-cache-dir -r requirements.txt
14
+
15
+ # Copy application code
16
+ COPY app.py token_reader.py entrypoint.sh ./
17
+
18
+ # Make entrypoint executable
19
+ RUN chmod +x entrypoint.sh
20
+
21
+ # Create .aws directory structure
22
+ RUN mkdir -p /root/.aws/sso/cache
23
+
24
+ # Expose port
25
+ EXPOSE 8989
26
+
27
+ # Health check
28
+ HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
29
+ CMD curl -f http://localhost:8989/health || exit 1
30
+
31
+ # Use entrypoint script for smart startup
32
+ ENTRYPOINT ["./entrypoint.sh"]
app.py ADDED
@@ -0,0 +1,305 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import time
4
+ import uuid
5
+ import httpx
6
+ from fastapi import FastAPI, HTTPException, Request
7
+ from fastapi.responses import StreamingResponse
8
+ from pydantic import BaseModel, Field
9
+ from typing import List, Optional, Dict, Any
10
+ from dotenv import load_dotenv
11
+
12
+ # Load environment variables
13
+ load_dotenv()
14
+
15
+ # Initialize FastAPI app
16
+ app = FastAPI(
17
+ title="Ki2API - Claude Sonnet 4 OpenAI Compatible API",
18
+ description="Simple Docker-ready OpenAI-compatible API for Claude Sonnet 4",
19
+ version="1.0.0"
20
+ )
21
+
22
+ # Configuration
23
+ API_KEY = os.getenv("API_KEY", "ki2api-key-2024")
24
+ KIRO_ACCESS_TOKEN = os.getenv("KIRO_ACCESS_TOKEN")
25
+ KIRO_REFRESH_TOKEN = os.getenv("KIRO_REFRESH_TOKEN")
26
+ KIRO_BASE_URL = "https://codewhisperer.us-east-1.amazonaws.com/generateAssistantResponse"
27
+ PROFILE_ARN = "arn:aws:codewhisperer:us-east-1:699475941385:profile/EHGA3GRVQMUK"
28
+
29
+ # Model mapping
30
+ MODEL_NAME = "claude-sonnet-4-20250514"
31
+ CODEWHISPERER_MODEL = "CLAUDE_SONNET_4_20250514_V1_0"
32
+
33
+ # Pydantic models
34
+ class ChatMessage(BaseModel):
35
+ role: str
36
+ content: str
37
+
38
+ class ChatCompletionRequest(BaseModel):
39
+ model: str
40
+ messages: List[ChatMessage]
41
+ temperature: Optional[float] = 0.7
42
+ max_tokens: Optional[int] = 4000
43
+ stream: Optional[bool] = False
44
+
45
+ class ChatCompletionResponse(BaseModel):
46
+ id: str = Field(default_factory=lambda: f"chatcmpl-{uuid.uuid4()}")
47
+ object: str = "chat.completion"
48
+ created: int = Field(default_factory=lambda: int(time.time()))
49
+ model: str
50
+ choices: List[Dict[str, Any]]
51
+ usage: Dict[str, int]
52
+
53
+ class ChatCompletionStreamResponse(BaseModel):
54
+ id: str = Field(default_factory=lambda: f"chatcmpl-{uuid.uuid4()}")
55
+ object: str = "chat.completion.chunk"
56
+ created: int = Field(default_factory=lambda: int(time.time()))
57
+ model: str
58
+ choices: List[Dict[str, Any]]
59
+
60
+ # Token management
61
+ class TokenManager:
62
+ def __init__(self):
63
+ self.access_token = KIRO_ACCESS_TOKEN
64
+ self.refresh_token = KIRO_REFRESH_TOKEN
65
+ self.refresh_url = "https://prod.us-east-1.auth.desktop.kiro.dev/refreshToken"
66
+
67
+ async def refresh_tokens(self):
68
+ if not self.refresh_token:
69
+ return None
70
+
71
+ try:
72
+ async with httpx.AsyncClient() as client:
73
+ response = await client.post(
74
+ self.refresh_url,
75
+ json={"refreshToken": self.refresh_token},
76
+ timeout=30
77
+ )
78
+ response.raise_for_status()
79
+
80
+ data = response.json()
81
+ self.access_token = data.get("accessToken")
82
+ return self.access_token
83
+ except Exception as e:
84
+ print(f"Token refresh failed: {e}")
85
+ return None
86
+
87
+ def get_token(self):
88
+ return self.access_token
89
+
90
+ token_manager = TokenManager()
91
+
92
+ # Build CodeWhisperer request
93
+ def build_codewhisperer_request(messages: List[ChatMessage]):
94
+ conversation_id = str(uuid.uuid4())
95
+
96
+ # Extract system prompt and user messages
97
+ system_prompt = ""
98
+ user_messages = []
99
+
100
+ for msg in messages:
101
+ if msg.role == "system":
102
+ system_prompt = msg.content
103
+ else:
104
+ user_messages.append(msg)
105
+
106
+ if not user_messages:
107
+ raise HTTPException(status_code=400, detail="No user messages found")
108
+
109
+ # Build history
110
+ history = []
111
+ for i in range(0, len(user_messages) - 1, 2):
112
+ if i + 1 < len(user_messages):
113
+ history.append({
114
+ "userInputMessage": {
115
+ "content": user_messages[i].content,
116
+ "modelId": CODEWHISPERER_MODEL,
117
+ "origin": "AI_EDITOR"
118
+ }
119
+ })
120
+ history.append({
121
+ "assistantResponseMessage": {
122
+ "content": user_messages[i + 1].content,
123
+ "toolUses": []
124
+ }
125
+ })
126
+
127
+ # Build current message
128
+ current_message = user_messages[-1]
129
+ content = current_message.content
130
+ if system_prompt:
131
+ content = f"{system_prompt}\n\n{content}"
132
+
133
+ return {
134
+ "profileArn": PROFILE_ARN,
135
+ "conversationState": {
136
+ "chatTriggerType": "MANUAL",
137
+ "conversationId": conversation_id,
138
+ "currentMessage": {
139
+ "userInputMessage": {
140
+ "content": content,
141
+ "modelId": CODEWHISPERER_MODEL,
142
+ "origin": "AI_EDITOR",
143
+ "userInputMessageContext": {}
144
+ }
145
+ },
146
+ "history": history
147
+ }
148
+ }
149
+
150
+ # Make API call to Kiro/CodeWhisperer
151
+ async def call_kiro_api(messages: List[ChatMessage], stream: bool = False):
152
+ token = token_manager.get_token()
153
+ if not token:
154
+ raise HTTPException(status_code=401, detail="No access token available")
155
+
156
+ request_data = build_codewhisperer_request(messages)
157
+
158
+ headers = {
159
+ "Authorization": f"Bearer {token}",
160
+ "Content-Type": "application/json",
161
+ "Accept": "text/event-stream" if stream else "application/json"
162
+ }
163
+
164
+ try:
165
+ async with httpx.AsyncClient() as client:
166
+ response = await client.post(
167
+ KIRO_BASE_URL,
168
+ headers=headers,
169
+ json=request_data,
170
+ timeout=120
171
+ )
172
+
173
+ if response.status_code == 403:
174
+ # Try to refresh token
175
+ new_token = await token_manager.refresh_tokens()
176
+ if new_token:
177
+ headers["Authorization"] = f"Bearer {new_token}"
178
+ response = await client.post(
179
+ KIRO_BASE_URL,
180
+ headers=headers,
181
+ json=request_data,
182
+ timeout=120
183
+ )
184
+
185
+ response.raise_for_status()
186
+ return response
187
+
188
+ except Exception as e:
189
+ raise HTTPException(status_code=503, detail=f"API call failed: {str(e)}")
190
+
191
+ # API endpoints
192
+ @app.get("/v1/models")
193
+ async def list_models():
194
+ return {
195
+ "object": "list",
196
+ "data": [
197
+ {
198
+ "id": MODEL_NAME,
199
+ "object": "model",
200
+ "created": int(time.time()),
201
+ "owned_by": "ki2api"
202
+ }
203
+ ]
204
+ }
205
+
206
+ @app.post("/v1/chat/completions")
207
+ async def create_chat_completion(request: ChatCompletionRequest):
208
+ if request.model != MODEL_NAME:
209
+ raise HTTPException(status_code=400, detail=f"Only {MODEL_NAME} is supported")
210
+
211
+ if request.stream:
212
+ return await create_streaming_response(request)
213
+ else:
214
+ return await create_non_streaming_response(request)
215
+
216
+ async def create_non_streaming_response(request: ChatCompletionRequest):
217
+ response = await call_kiro_api(request.messages, stream=False)
218
+
219
+ # Parse response
220
+ response_text = response.text
221
+
222
+ return ChatCompletionResponse(
223
+ model=MODEL_NAME,
224
+ choices=[{
225
+ "index": 0,
226
+ "message": {
227
+ "role": "assistant",
228
+ "content": response_text
229
+ },
230
+ "finish_reason": "stop"
231
+ }],
232
+ usage={
233
+ "prompt_tokens": 0,
234
+ "completion_tokens": 0,
235
+ "total_tokens": 0
236
+ }
237
+ )
238
+
239
+ async def create_streaming_response(request: ChatCompletionRequest):
240
+ response = await call_kiro_api(request.messages, stream=True)
241
+
242
+ async def generate():
243
+ # Send initial response
244
+ initial_chunk = {
245
+ 'id': f'chatcmpl-{uuid.uuid4()}',
246
+ 'object': 'chat.completion.chunk',
247
+ 'created': int(time.time()),
248
+ 'model': MODEL_NAME,
249
+ 'choices': [{
250
+ 'index': 0,
251
+ 'delta': {'role': 'assistant'},
252
+ 'finish_reason': None
253
+ }]
254
+ }
255
+ yield f"data: {json.dumps(initial_chunk)}\n\n"
256
+
257
+ # Read response and stream content
258
+ content = ""
259
+ async for line in response.aiter_lines():
260
+ if line.startswith('data: '):
261
+ try:
262
+ data = json.loads(line[6:])
263
+ if 'content' in data:
264
+ content += data['content']
265
+ chunk = {
266
+ 'id': f'chatcmpl-{uuid.uuid4()}',
267
+ 'object': 'chat.completion.chunk',
268
+ 'created': int(time.time()),
269
+ 'model': MODEL_NAME,
270
+ 'choices': [{
271
+ 'index': 0,
272
+ 'delta': {'content': data['content']},
273
+ 'finish_reason': None
274
+ }]
275
+ }
276
+ yield f"data: {json.dumps(chunk)}\n\n"
277
+ except:
278
+ continue
279
+
280
+ # Send final response
281
+ final_chunk = {
282
+ 'id': f'chatcmpl-{uuid.uuid4()}',
283
+ 'object': 'chat.completion.chunk',
284
+ 'created': int(time.time()),
285
+ 'model': MODEL_NAME,
286
+ 'choices': [{
287
+ 'index': 0,
288
+ 'delta': {},
289
+ 'finish_reason': 'stop'
290
+ }]
291
+ }
292
+ yield f"data: {json.dumps(final_chunk)}\n\n"
293
+
294
+ yield "data: [DONE]\n\n"
295
+
296
+ return StreamingResponse(generate(), media_type="text/event-stream")
297
+
298
+ # Health check
299
+ @app.get("/health")
300
+ async def health_check():
301
+ return {"status": "ok", "service": "ki2api"}
302
+
303
+ if __name__ == "__main__":
304
+ import uvicorn
305
+ uvicorn.run(app, host="0.0.0.0", port=8989)
docker-compose.yml ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ ki2api:
5
+ build: .
6
+ ports:
7
+ - "8989:8989"
8
+ environment:
9
+ - API_KEY=ki2api-key-2024
10
+ volumes:
11
+ # 自动挂载token目录,实现零配置
12
+ - ~/.aws/sso/cache:/root/.aws/sso/cache:ro
13
+ restart: unless-stopped
14
+ healthcheck:
15
+ test: ["CMD", "curl", "-f", "http://localhost:8989/health"]
16
+ interval: 30s
17
+ timeout: 10s
18
+ retries: 3
19
+ start_period: 40s
entrypoint.sh ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # 智能Docker入口脚本
4
+ # 自动读取token并启动服务
5
+
6
+ echo "🚀 Ki2API 启动中..."
7
+
8
+ # 检查是否存在token文件
9
+ TOKEN_FILE="/root/.aws/sso/cache/kiro-auth-token.json"
10
+
11
+ if [ -f "$TOKEN_FILE" ]; then
12
+ echo "📁 发现token文件,正在读取..."
13
+
14
+ # 运行token读取脚本
15
+ python token_reader.py
16
+
17
+ if [ $? -eq 0 ]; then
18
+ echo "✅ Token配置完成"
19
+ else
20
+ echo "⚠️ Token读取失败,继续启动(需要手动配置token)"
21
+ fi
22
+ else
23
+ echo "⚠️ 未找到token文件: $TOKEN_FILE"
24
+ echo "请确保已登录Kiro,或手动设置环境变量"
25
+ fi
26
+
27
+ # 检查环境变量
28
+ if [ -z "$KIRO_ACCESS_TOKEN" ] || [ -z "$KIRO_REFRESH_TOKEN" ]; then
29
+ echo "⚠️ 环境变量未设置,尝试从.env文件加载..."
30
+ if [ -f ".env" ]; then
31
+ export $(cat .env | xargs)
32
+ echo "✅ 已从.env文件加载token"
33
+ else
34
+ echo "❌ 未找到token配置,服务可能无法正常工作"
35
+ echo "请设置 KIRO_ACCESS_TOKEN 和 KIRO_REFRESH_TOKEN 环境变量"
36
+ fi
37
+ fi
38
+
39
+ # 启动应用
40
+ echo "🎯 启动FastAPI服务..."
41
+ exec python app.py
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ fastapi==0.104.1
2
+ uvicorn[standard]==0.24.0
3
+ httpx==0.25.2
4
+ python-dotenv==1.0.0
5
+ pydantic==2.5.0
token_reader.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ 自动读取Kiro token的脚本
4
+ 在Docker容器启动时自动读取宿主机的token文件
5
+ """
6
+
7
+ import os
8
+ import json
9
+ import sys
10
+ from pathlib import Path
11
+
12
+ def get_token_file_path():
13
+ """获取token文件路径"""
14
+ home = Path.home()
15
+ return home / ".aws" / "sso" / "cache" / "kiro-auth-token.json"
16
+
17
+ def read_tokens():
18
+ """读取token文件"""
19
+ token_file = get_token_file_path()
20
+
21
+ if not token_file.exists():
22
+ print(f"❌ Token文件不存在: {token_file}")
23
+ print("请确保已登录Kiro,或手动创建token文件")
24
+ return None, None
25
+
26
+ try:
27
+ with open(token_file, 'r', encoding='utf-8') as f:
28
+ data = json.load(f)
29
+
30
+ access_token = data.get('accessToken')
31
+ refresh_token = data.get('refreshToken')
32
+
33
+ if not access_token or not refresh_token:
34
+ print("❌ Token文件格式错误,缺少accessToken或refreshToken")
35
+ return None, None
36
+
37
+ return access_token, refresh_token
38
+
39
+ except json.JSONDecodeError:
40
+ print("❌ Token文件JSON格式错误")
41
+ return None, None
42
+ except Exception as e:
43
+ print(f"❌ 读取token文件失败: {e}")
44
+ return None, None
45
+
46
+ def create_env_file(access_token, refresh_token):
47
+ """创建.env文件"""
48
+ env_content = f"""# Kiro Token配置
49
+ # 自动生成于 {os.path.basename(__file__)}
50
+ KIRO_ACCESS_TOKEN={access_token}
51
+ KIRO_REFRESH_TOKEN={refresh_token}
52
+ """
53
+
54
+ with open('.env', 'w', encoding='utf-8') as f:
55
+ f.write(env_content)
56
+
57
+ print("✅ .env文件已创建/更新")
58
+
59
+ def main():
60
+ """主函数"""
61
+ print("🔍 正在读取Kiro token...")
62
+
63
+ access_token, refresh_token = read_tokens()
64
+
65
+ if access_token and refresh_token:
66
+ create_env_file(access_token, refresh_token)
67
+ print("✅ Token读取成功,服务即将启动...")
68
+ return 0
69
+ else:
70
+ print("❌ 无法获取token,请检查:")
71
+ print("1. 是否已登录Kiro (https://kiro.dev)")
72
+ print("2. token文件是否存在: ~/.aws/sso/cache/kiro-auth-token.json")
73
+ print("3. 或手动创建.env文件并设置token")
74
+ return 1
75
+
76
+ if __name__ == "__main__":
77
+ sys.exit(main())