router-maestro 0.1.7__tar.gz → 0.1.8__tar.gz

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.
Files changed (72) hide show
  1. {router_maestro-0.1.7 → router_maestro-0.1.8}/CLAUDE.md +9 -0
  2. {router_maestro-0.1.7 → router_maestro-0.1.8}/PKG-INFO +1 -1
  3. {router_maestro-0.1.7 → router_maestro-0.1.8}/pyproject.toml +1 -1
  4. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/__init__.py +1 -1
  5. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/providers/copilot.py +12 -10
  6. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/server/routes/responses.py +109 -20
  7. {router_maestro-0.1.7 → router_maestro-0.1.8}/uv.lock +1 -1
  8. {router_maestro-0.1.7 → router_maestro-0.1.8}/.env.example +0 -0
  9. {router_maestro-0.1.7 → router_maestro-0.1.8}/.github/workflows/ci.yml +0 -0
  10. {router_maestro-0.1.7 → router_maestro-0.1.8}/.github/workflows/release.yml +0 -0
  11. {router_maestro-0.1.7 → router_maestro-0.1.8}/.gitignore +0 -0
  12. {router_maestro-0.1.7 → router_maestro-0.1.8}/.markdownlint.json +0 -0
  13. {router_maestro-0.1.7 → router_maestro-0.1.8}/Dockerfile +0 -0
  14. {router_maestro-0.1.7 → router_maestro-0.1.8}/LICENSE +0 -0
  15. {router_maestro-0.1.7 → router_maestro-0.1.8}/Makefile +0 -0
  16. {router_maestro-0.1.7 → router_maestro-0.1.8}/README.md +0 -0
  17. {router_maestro-0.1.7 → router_maestro-0.1.8}/docker-compose.yml +0 -0
  18. {router_maestro-0.1.7 → router_maestro-0.1.8}/docs/deployment.md +0 -0
  19. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/__main__.py +0 -0
  20. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/auth/__init__.py +0 -0
  21. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/auth/github_oauth.py +0 -0
  22. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/auth/manager.py +0 -0
  23. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/auth/storage.py +0 -0
  24. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/cli/__init__.py +0 -0
  25. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/cli/auth.py +0 -0
  26. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/cli/client.py +0 -0
  27. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/cli/config.py +0 -0
  28. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/cli/context.py +0 -0
  29. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/cli/main.py +0 -0
  30. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/cli/model.py +0 -0
  31. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/cli/server.py +0 -0
  32. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/config/__init__.py +0 -0
  33. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/config/contexts.py +0 -0
  34. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/config/paths.py +0 -0
  35. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/config/priorities.py +0 -0
  36. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/config/providers.py +0 -0
  37. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/config/server.py +0 -0
  38. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/config/settings.py +0 -0
  39. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/providers/__init__.py +0 -0
  40. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/providers/anthropic.py +0 -0
  41. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/providers/base.py +0 -0
  42. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/providers/openai.py +0 -0
  43. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/providers/openai_compat.py +0 -0
  44. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/routing/__init__.py +0 -0
  45. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/routing/router.py +0 -0
  46. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/server/__init__.py +0 -0
  47. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/server/app.py +0 -0
  48. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/server/middleware/__init__.py +0 -0
  49. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/server/middleware/auth.py +0 -0
  50. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/server/oauth_sessions.py +0 -0
  51. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/server/routes/__init__.py +0 -0
  52. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/server/routes/admin.py +0 -0
  53. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/server/routes/anthropic.py +0 -0
  54. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/server/routes/chat.py +0 -0
  55. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/server/routes/models.py +0 -0
  56. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/server/schemas/__init__.py +0 -0
  57. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/server/schemas/admin.py +0 -0
  58. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/server/schemas/anthropic.py +0 -0
  59. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/server/schemas/openai.py +0 -0
  60. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/server/schemas/responses.py +0 -0
  61. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/server/translation.py +0 -0
  62. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/utils/__init__.py +0 -0
  63. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/utils/logging.py +0 -0
  64. {router_maestro-0.1.7 → router_maestro-0.1.8}/src/router_maestro/utils/tokens.py +0 -0
  65. {router_maestro-0.1.7 → router_maestro-0.1.8}/tests/__init__.py +0 -0
  66. {router_maestro-0.1.7 → router_maestro-0.1.8}/tests/test_anthropic_models.py +0 -0
  67. {router_maestro-0.1.7 → router_maestro-0.1.8}/tests/test_auth.py +0 -0
  68. {router_maestro-0.1.7 → router_maestro-0.1.8}/tests/test_config.py +0 -0
  69. {router_maestro-0.1.7 → router_maestro-0.1.8}/tests/test_providers.py +0 -0
  70. {router_maestro-0.1.7 → router_maestro-0.1.8}/tests/test_router.py +0 -0
  71. {router_maestro-0.1.7 → router_maestro-0.1.8}/tests/test_translation.py +0 -0
  72. {router_maestro-0.1.7 → router_maestro-0.1.8}/tests/test_utils.py +0 -0
@@ -77,3 +77,12 @@ Configuration and data files follow XDG conventions:
77
77
  ### Model Identification
78
78
 
79
79
  Models are identified by `provider/model-id` format (e.g., `github-copilot/gpt-4o`). The special model name `router-maestro` triggers auto-routing based on priority configuration.
80
+
81
+ ### Version Updates
82
+
83
+ When releasing a new version, update these files:
84
+
85
+ 1. `pyproject.toml` - `version = "x.x.x"`
86
+ 2. `src/router_maestro/__init__.py` - `__version__ = "x.x.x"`
87
+ 3. Run `uv lock` to update `uv.lock`
88
+ 4. Create git tag: `git tag vx.x.x`
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: router-maestro
3
- Version: 0.1.7
3
+ Version: 0.1.8
4
4
  Summary: Multi-model routing and load balancing system with OpenAI-compatible API
5
5
  Author-email: Kanwen Li <likanwen@icloud.com>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "router-maestro"
3
- version = "0.1.7"
3
+ version = "0.1.8"
4
4
  description = "Multi-model routing and load balancing system with OpenAI-compatible API"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1,3 +1,3 @@
1
1
  """Router-Maestro: Multi-model routing and load balancing system."""
2
2
 
3
- __version__ = "0.1.7"
3
+ __version__ = "0.1.8"
@@ -618,16 +618,18 @@ class CopilotProvider(BaseProvider):
618
618
  elif event_type == "response.output_item.done":
619
619
  item = data.get("item", {})
620
620
  if item.get("type") == "function_call":
621
- # Emit complete tool call if not already done
622
- yield ResponsesStreamChunk(
623
- content="",
624
- tool_call=ResponsesToolCall(
625
- call_id=item.get("call_id", ""),
626
- name=item.get("name", ""),
627
- arguments=item.get("arguments", "{}"),
628
- ),
629
- )
630
- current_fc = None
621
+ # Only emit if not already done via function_call_arguments.done
622
+ # (current_fc would be None if already emitted)
623
+ if current_fc is not None:
624
+ yield ResponsesStreamChunk(
625
+ content="",
626
+ tool_call=ResponsesToolCall(
627
+ call_id=item.get("call_id", ""),
628
+ name=item.get("name", ""),
629
+ arguments=item.get("arguments", "{}"),
630
+ ),
631
+ )
632
+ current_fc = None
631
633
 
632
634
  # Handle done event to get final usage
633
635
  elif event_type == "response.done":
@@ -1,6 +1,7 @@
1
1
  """Responses API route for Codex models."""
2
2
 
3
3
  import json
4
+ import time
4
5
  import uuid
5
6
  from collections.abc import AsyncGenerator
6
7
  from typing import Any
@@ -163,6 +164,23 @@ def make_text_content(text: str) -> dict[str, Any]:
163
164
  return {"type": "output_text", "text": text, "annotations": []}
164
165
 
165
166
 
167
+ def make_usage(raw_usage: dict[str, Any] | None) -> dict[str, Any] | None:
168
+ """Create properly structured usage object matching OpenAI spec."""
169
+ if not raw_usage:
170
+ return None
171
+
172
+ input_tokens = raw_usage.get("input_tokens", 0)
173
+ output_tokens = raw_usage.get("output_tokens", 0)
174
+
175
+ return {
176
+ "input_tokens": input_tokens,
177
+ "input_tokens_details": {"cached_tokens": 0},
178
+ "output_tokens": output_tokens,
179
+ "output_tokens_details": {"reasoning_tokens": 0},
180
+ "total_tokens": input_tokens + output_tokens,
181
+ }
182
+
183
+
166
184
  def make_message_item(msg_id: str, text: str, status: str = "completed") -> dict[str, Any]:
167
185
  """Create message output item."""
168
186
  return {
@@ -191,12 +209,17 @@ def make_function_call_item(
191
209
  @router.post("/api/openai/v1/responses")
192
210
  async def create_response(request: ResponsesRequest):
193
211
  """Handle Responses API requests (for Codex models)."""
212
+ request_id = generate_id("req")
213
+ start_time = time.time()
214
+
194
215
  logger.info(
195
- "Received responses request: model=%s, stream=%s, has_tools=%s",
216
+ "Received responses request: req_id=%s, model=%s, stream=%s, has_tools=%s",
217
+ request_id,
196
218
  request.model,
197
219
  request.stream,
198
220
  request.tools is not None,
199
221
  )
222
+
200
223
  model_router = get_router()
201
224
 
202
225
  input_value = convert_input_to_internal(request.input)
@@ -215,8 +238,13 @@ async def create_response(request: ResponsesRequest):
215
238
 
216
239
  if request.stream:
217
240
  return StreamingResponse(
218
- stream_response(model_router, internal_request),
241
+ stream_response(model_router, internal_request, request_id, start_time),
219
242
  media_type="text/event-stream",
243
+ headers={
244
+ "Cache-Control": "no-cache",
245
+ "Connection": "keep-alive",
246
+ "X-Accel-Buffering": "no",
247
+ },
220
248
  )
221
249
 
222
250
  try:
@@ -250,17 +278,44 @@ async def create_response(request: ResponsesRequest):
250
278
  usage=usage,
251
279
  )
252
280
  except ProviderError as e:
253
- logger.error("Responses request failed: %s", e)
281
+ elapsed_ms = (time.time() - start_time) * 1000
282
+ logger.error(
283
+ "Responses request failed: req_id=%s, elapsed=%.1fms, error=%s",
284
+ request_id,
285
+ elapsed_ms,
286
+ e,
287
+ )
254
288
  raise HTTPException(status_code=e.status_code, detail=str(e))
255
289
 
256
290
 
257
291
  async def stream_response(
258
- model_router: Router, request: InternalResponsesRequest
292
+ model_router: Router,
293
+ request: InternalResponsesRequest,
294
+ request_id: str,
295
+ start_time: float,
259
296
  ) -> AsyncGenerator[str, None]:
260
297
  """Stream Responses API response."""
261
298
  try:
262
299
  stream, provider_name = await model_router.responses_completion_stream(request)
263
300
  response_id = generate_id("resp")
301
+ created_at = int(time.time())
302
+
303
+ logger.debug(
304
+ "Stream started: req_id=%s, resp_id=%s, provider=%s",
305
+ request_id,
306
+ response_id,
307
+ provider_name,
308
+ )
309
+
310
+ # Base response object with all required fields (matching OpenAI spec)
311
+ base_response = {
312
+ "id": response_id,
313
+ "object": "response",
314
+ "created_at": created_at,
315
+ "model": request.model,
316
+ "error": None,
317
+ "incomplete_details": None,
318
+ }
264
319
 
265
320
  output_items: list[dict[str, Any]] = []
266
321
  output_index = 0
@@ -278,8 +333,7 @@ async def stream_response(
278
333
  {
279
334
  "type": "response.created",
280
335
  "response": {
281
- "id": response_id,
282
- "object": "response",
336
+ **base_response,
283
337
  "status": "in_progress",
284
338
  "output": [],
285
339
  },
@@ -291,8 +345,7 @@ async def stream_response(
291
345
  {
292
346
  "type": "response.in_progress",
293
347
  "response": {
294
- "id": response_id,
295
- "object": "response",
348
+ **base_response,
296
349
  "status": "in_progress",
297
350
  "output": [],
298
351
  },
@@ -306,11 +359,18 @@ async def stream_response(
306
359
  current_message_id = generate_id("msg")
307
360
  message_started = True
308
361
 
362
+ # Note: content starts as empty array, matching OpenAI spec
309
363
  yield sse_event(
310
364
  {
311
365
  "type": "response.output_item.added",
312
366
  "output_index": output_index,
313
- "item": make_message_item(current_message_id, "", "in_progress"),
367
+ "item": {
368
+ "type": "message",
369
+ "id": current_message_id,
370
+ "role": "assistant",
371
+ "content": [],
372
+ "status": "in_progress",
373
+ },
314
374
  }
315
375
  )
316
376
 
@@ -440,12 +500,10 @@ async def stream_response(
440
500
  {
441
501
  "type": "response.completed",
442
502
  "response": {
443
- "id": response_id,
444
- "object": "response",
503
+ **base_response,
445
504
  "status": "completed",
446
- "model": request.model,
447
505
  "output": output_items,
448
- "usage": final_usage,
506
+ "usage": make_usage(final_usage),
449
507
  },
450
508
  }
451
509
  )
@@ -467,21 +525,52 @@ async def stream_response(
467
525
  {
468
526
  "type": "response.completed",
469
527
  "response": {
470
- "id": response_id,
471
- "object": "response",
528
+ **base_response,
472
529
  "status": "completed",
473
- "model": request.model,
474
530
  "output": output_items,
475
- "usage": final_usage,
531
+ "usage": make_usage(final_usage),
476
532
  },
477
533
  }
478
534
  )
479
535
 
480
- yield "data: [DONE]\n\n"
536
+ elapsed_ms = (time.time() - start_time) * 1000
537
+ logger.info(
538
+ "Stream completed: req_id=%s, elapsed=%.1fms, output_items=%d",
539
+ request_id,
540
+ elapsed_ms,
541
+ len(output_items),
542
+ )
543
+
544
+ # NOTE: Do NOT send "data: [DONE]\n\n" - agent-maestro doesn't send it
545
+ # for Responses API
481
546
 
482
547
  except ProviderError as e:
483
- error_data = {"error": {"message": str(e), "type": "provider_error"}}
484
- yield f"data: {json.dumps(error_data)}\n\n"
548
+ elapsed_ms = (time.time() - start_time) * 1000
549
+ logger.error(
550
+ "Stream failed: req_id=%s, elapsed=%.1fms, error=%s",
551
+ request_id,
552
+ elapsed_ms,
553
+ e,
554
+ )
555
+ # Send response.failed event matching OpenAI spec
556
+ yield sse_event(
557
+ {
558
+ "type": "response.failed",
559
+ "response": {
560
+ "id": response_id,
561
+ "object": "response",
562
+ "status": "failed",
563
+ "created_at": created_at,
564
+ "model": request.model,
565
+ "output": [],
566
+ "error": {
567
+ "code": "server_error",
568
+ "message": str(e),
569
+ },
570
+ "incomplete_details": None,
571
+ },
572
+ }
573
+ )
485
574
 
486
575
 
487
576
  def _close_message_events(
@@ -925,7 +925,7 @@ wheels = [
925
925
 
926
926
  [[package]]
927
927
  name = "router-maestro"
928
- version = "0.1.7"
928
+ version = "0.1.8"
929
929
  source = { editable = "." }
930
930
  dependencies = [
931
931
  { name = "aiosqlite" },
File without changes
File without changes
File without changes