返回 CodeWhale
concentrate-stub.py
根目录 / scripts / concentrate-stub.py
1 #!/usr/bin/env python3
2 """Local Concentrate contract stub for keyless dogfood (no network, no account).
3
4 Speaks the documented surface of https://api.concentrate.ai/v1 well enough to
5 prove Codewhale's real request path end to end:
6
7 GET /v1/responses/health -> 200, empty body (unauthenticated)
8 GET /v1/models -> {"object":"list","data":[{"id":...}]} (unauthenticated)
9 POST /v1/responses -> typed `response.*` SSE events, no `[DONE]`
10
11 Contract sources (fetched 2026-08-29):
12 https://concentrate.ai/docs/api-reference/introduction
13 https://concentrate.ai/docs/api-reference/endpoint/request-parameters
14 https://concentrate.ai/docs/api-reference/endpoint/streaming
15 https://concentrate.ai/docs/api-reference/endpoint/errors
16 https://concentrate.ai/docs/api-reference/endpoint/list-models
17 https://concentrate.ai/docs/api-reference/endpoint/health
18
19 The stub asserts what a real gateway would enforce and what Codewhale must
20 send: a `Bearer` Authorization header equal to CONCENTRATE_STUB_EXPECT_KEY,
21 `model` passed through verbatim, `stream: true`, and no undocumented top-level
22 fields. Every request is appended as JSON to CONCENTRATE_STUB_LOG so the
23 driver can assert the receipt after the run. A wrong key answers with the
24 documented 401 body so the error path is exercised too.
25
26 Usage: CONCENTRATE_STUB_PORT=8790 CONCENTRATE_STUB_EXPECT_KEY=stub-key \
27 CONCENTRATE_STUB_LOG=/tmp/concentrate-stub.jsonl python3 scripts/concentrate-stub.py
28 """
29 from __future__ import annotations
30
31 import json
32 import os
33 import sys
34 import time
35 from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
36
37 PORT = int(os.environ.get("CONCENTRATE_STUB_PORT", "8790"))
38 EXPECT_KEY = os.environ.get("CONCENTRATE_STUB_EXPECT_KEY", "stub-key")
39 LOG = os.environ.get("CONCENTRATE_STUB_LOG", "")
40 REPLY_TEXT = os.environ.get("CONCENTRATE_STUB_REPLY", "ok from the concentrate stub")
41
42 # https://concentrate.ai/docs/api-reference/endpoint/request-parameters
43 DOCUMENTED_TOP_LEVEL = {
44 "model", "input", "max_output_tokens", "temperature", "top_p", "stream",
45 "text", "reasoning", "tools", "tool_choice", "parallel_tool_calls",
46 "routing", "cache_control", "prompt_cache_options",
47 }
48
49 # A slice of the live catalog shape read on 2026-08-29 (ids are plain; the
50 # upstream provider lives in `owned_by`).
51 MODELS = [
52 {"id": "deepseek-v4-pro", "object": "model", "owned_by": "deepseek", "type": "chat", "display_name": "DeepSeek V4 Pro"},
53 {"id": "gpt-5.6-sol", "object": "model", "owned_by": "openai", "type": "chat", "display_name": "GPT-5.6 Sol"},
54 {"id": "claude-fable-5", "object": "model", "owned_by": "anthropic", "type": "chat", "display_name": "Claude Fable 5"},
55 ]
56
57
58 def log_event(record: dict) -> None:
59 if not LOG:
60 return
61 with open(LOG, "a", encoding="utf-8") as handle:
62 handle.write(json.dumps(record) + "\n")
63
64
65 class Handler(BaseHTTPRequestHandler):
66 server_version = "concentrate-stub/0.1"
67
68 def log_message(self, fmt, *args): # quiet by default; the driver reads the JSONL log
69 if os.environ.get("CONCENTRATE_STUB_VERBOSE"):
70 sys.stderr.write("%s - %s\n" % (self.address_string(), fmt % args))
71
72 def _json(self, status: int, payload: dict | None, headers: dict | None = None) -> None:
73 body = b"" if payload is None else json.dumps(payload).encode()
74 self.send_response(status)
75 self.send_header("Content-Type", "application/json")
76 self.send_header("Content-Length", str(len(body)))
77 for key, value in (headers or {}).items():
78 self.send_header(key, value)
79 self.end_headers()
80 if body:
81 self.wfile.write(body)
82
83 def do_GET(self): # noqa: N802 (http.server API)
84 path = self.path.split("?", 1)[0].rstrip("/")
85 log_event({"method": "GET", "path": self.path, "authorization": self.headers.get("Authorization")})
86 if path == "/v1/responses/health":
87 # https://concentrate.ai/docs/api-reference/endpoint/health — 200, empty body, no auth.
88 self.send_response(200)
89 self.send_header("Content-Type", "application/json")
90 self.send_header("Content-Length", "0")
91 self.end_headers()
92 return
93 if path == "/v1/models":
94 # https://concentrate.ai/docs/api-reference/endpoint/list-models — no auth required.
95 self._json(200, {"object": "list", "data": MODELS})
96 return
97 self._json(404, {"error": "Not Found", "message": f"No route for {path}"})
98
99 def do_POST(self): # noqa: N802
100 path = self.path.split("?", 1)[0].rstrip("/")
101 length = int(self.headers.get("Content-Length") or 0)
102 raw = self.rfile.read(length) if length else b""
103 try:
104 body = json.loads(raw or b"{}")
105 except json.JSONDecodeError:
106 self._json(400, {"error": "Bad Request", "message": "Invalid JSON body"})
107 return
108 auth = self.headers.get("Authorization") or ""
109 record = {
110 "method": "POST",
111 "path": self.path,
112 "authorization": auth,
113 "model": body.get("model"),
114 "stream": body.get("stream"),
115 "top_level_fields": sorted(body.keys()),
116 "undocumented_fields": sorted(set(body.keys()) - DOCUMENTED_TOP_LEVEL),
117 "input_roles": [item.get("role") for item in body.get("input", []) if isinstance(item, dict)],
118 "tool_names": [tool.get("name") for tool in body.get("tools", []) if isinstance(tool, dict)],
119 }
120 log_event(record)
121 if path != "/v1/responses":
122 self._json(404, {"error": "Not Found", "message": f"No route for {path}"})
123 return
124 # https://concentrate.ai/docs/api-reference/endpoint/errors
125 if auth != f"Bearer {EXPECT_KEY}":
126 self._json(401, {"error": "Unauthorized", "message": "Invalid API key"})
127 return
128 if not body.get("model"):
129 self._json(400, {"error": "Bad Request", "message": "Invalid model name: ''"})
130 return
131 if record["undocumented_fields"]:
132 self._json(400, {"error": "Bad Request", "message": f"Invalid parameters: {record['undocumented_fields']}"})
133 return
134 if body.get("model") == "stub/insufficient-credits":
135 self._json(402, {"error": "Insufficient funds", "message": "Your account has insufficient credits. Please add credits to continue."})
136 return
137 if not body.get("stream"):
138 self._json(200, self._completed_response(body))
139 return
140 self._stream(body)
141
142 def _completed_response(self, body: dict) -> dict:
143 selected = body["model"] if "/" in body["model"] else f"stub/{body['model']}"
144 return {
145 "id": "resp_stub_1",
146 "object": "response",
147 "created_at": int(time.time()),
148 "status": "completed",
149 "model": selected,
150 "output": [{
151 "type": "message", "id": "msg_stub_1", "status": "completed", "role": "assistant",
152 "content": [{"type": "output_text", "text": REPLY_TEXT, "annotations": []}],
153 }],
154 "usage": {"input_tokens": 12, "output_tokens": 5, "total_tokens": 17,
155 "input_tokens_details": {"cached_tokens": 0}},
156 }
157
158 def _stream(self, body: dict) -> None:
159 # https://concentrate.ai/docs/api-reference/endpoint/streaming — typed
160 # events, `event:` + `data:` frames, sequence numbers, no [DONE].
161 response = self._completed_response(body)
162 self.send_response(200)
163 self.send_header("Content-Type", "text/event-stream")
164 self.send_header("Cache-Control", "no-cache")
165 self.end_headers()
166 seq = 0
167
168 def emit(event_type: str, payload: dict) -> None:
169 nonlocal seq
170 payload = {"type": event_type, "sequence_number": seq, **payload}
171 seq += 1
172 self.wfile.write(f"event: {event_type}\ndata: {json.dumps(payload)}\n\n".encode())
173 self.wfile.flush()
174
175 in_progress = {**response, "status": "in_progress", "output": [], "usage": None}
176 emit("response.created", {"response": in_progress})
177 emit("response.in_progress", {"response": in_progress})
178 item = {"type": "message", "id": "msg_stub_1", "status": "in_progress", "role": "assistant", "content": []}
179 emit("response.output_item.added", {"output_index": 0, "item": item})
180 emit("response.content_part.added", {"item_id": "msg_stub_1", "output_index": 0, "content_index": 0,
181 "part": {"type": "output_text", "text": "", "annotations": []}})
182 words = REPLY_TEXT.split(" ")
183 for index, word in enumerate(words):
184 delta = word if index == len(words) - 1 else word + " "
185 emit("response.output_text.delta", {"item_id": "msg_stub_1", "output_index": 0, "content_index": 0, "delta": delta})
186 emit("response.output_text.done", {"item_id": "msg_stub_1", "output_index": 0, "content_index": 0, "text": REPLY_TEXT})
187 emit("response.content_part.done", {"item_id": "msg_stub_1", "output_index": 0, "content_index": 0,
188 "part": {"type": "output_text", "text": REPLY_TEXT, "annotations": []}})
189 emit("response.output_item.done", {"output_index": 0, "item": response["output"][0]})
190 emit("response.completed", {"response": response})
191
192
193 def main() -> int:
194 server = ThreadingHTTPServer(("127.0.0.1", PORT), Handler)
195 print(f"concentrate-stub listening on http://127.0.0.1:{server.server_address[1]}/v1", flush=True)
196 try:
197 server.serve_forever()
198 except KeyboardInterrupt:
199 pass
200 return 0
201
202
203 if __name__ == "__main__":
204 sys.exit(main())
205
205 lines PYTHON