ccproxy-api 0.1.2__py3-none-any.whl → 0.1.3__py3-none-any.whl
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.
- ccproxy/_version.py +2 -2
- ccproxy/adapters/openai/__init__.py +1 -2
- ccproxy/adapters/openai/adapter.py +218 -180
- ccproxy/adapters/openai/streaming.py +247 -65
- ccproxy/api/__init__.py +0 -3
- ccproxy/api/app.py +173 -40
- ccproxy/api/dependencies.py +62 -3
- ccproxy/api/middleware/errors.py +3 -7
- ccproxy/api/middleware/headers.py +0 -2
- ccproxy/api/middleware/logging.py +4 -3
- ccproxy/api/middleware/request_content_logging.py +297 -0
- ccproxy/api/middleware/request_id.py +5 -0
- ccproxy/api/middleware/server_header.py +0 -4
- ccproxy/api/routes/__init__.py +9 -1
- ccproxy/api/routes/claude.py +23 -32
- ccproxy/api/routes/health.py +58 -4
- ccproxy/api/routes/mcp.py +171 -0
- ccproxy/api/routes/metrics.py +4 -8
- ccproxy/api/routes/permissions.py +217 -0
- ccproxy/api/routes/proxy.py +0 -53
- ccproxy/api/services/__init__.py +6 -0
- ccproxy/api/services/permission_service.py +368 -0
- ccproxy/api/ui/__init__.py +6 -0
- ccproxy/api/ui/permission_handler_protocol.py +33 -0
- ccproxy/api/ui/terminal_permission_handler.py +593 -0
- ccproxy/auth/conditional.py +2 -2
- ccproxy/auth/dependencies.py +1 -1
- ccproxy/auth/oauth/models.py +0 -1
- ccproxy/auth/oauth/routes.py +1 -3
- ccproxy/auth/storage/json_file.py +0 -1
- ccproxy/auth/storage/keyring.py +0 -3
- ccproxy/claude_sdk/__init__.py +2 -0
- ccproxy/claude_sdk/client.py +91 -8
- ccproxy/claude_sdk/converter.py +405 -210
- ccproxy/claude_sdk/options.py +76 -29
- ccproxy/claude_sdk/parser.py +200 -0
- ccproxy/claude_sdk/streaming.py +286 -0
- ccproxy/cli/commands/__init__.py +5 -2
- ccproxy/cli/commands/auth.py +2 -4
- ccproxy/cli/commands/permission_handler.py +553 -0
- ccproxy/cli/commands/serve.py +30 -12
- ccproxy/cli/docker/params.py +0 -4
- ccproxy/cli/helpers.py +0 -2
- ccproxy/cli/main.py +5 -16
- ccproxy/cli/options/claude_options.py +19 -1
- ccproxy/cli/options/core_options.py +0 -3
- ccproxy/cli/options/security_options.py +0 -2
- ccproxy/cli/options/server_options.py +3 -2
- ccproxy/config/auth.py +0 -1
- ccproxy/config/claude.py +78 -2
- ccproxy/config/discovery.py +0 -1
- ccproxy/config/docker_settings.py +0 -1
- ccproxy/config/loader.py +1 -4
- ccproxy/config/scheduler.py +20 -0
- ccproxy/config/security.py +7 -2
- ccproxy/config/server.py +5 -0
- ccproxy/config/settings.py +13 -7
- ccproxy/config/validators.py +1 -1
- ccproxy/core/async_utils.py +1 -4
- ccproxy/core/errors.py +45 -1
- ccproxy/core/http_transformers.py +4 -3
- ccproxy/core/interfaces.py +2 -2
- ccproxy/core/logging.py +97 -95
- ccproxy/core/middleware.py +1 -1
- ccproxy/core/proxy.py +1 -1
- ccproxy/core/transformers.py +1 -1
- ccproxy/core/types.py +1 -1
- ccproxy/docker/models.py +1 -1
- ccproxy/docker/protocol.py +0 -3
- ccproxy/models/__init__.py +41 -0
- ccproxy/models/claude_sdk.py +420 -0
- ccproxy/models/messages.py +45 -18
- ccproxy/models/permissions.py +115 -0
- ccproxy/models/requests.py +1 -1
- ccproxy/models/responses.py +29 -2
- ccproxy/observability/access_logger.py +1 -2
- ccproxy/observability/context.py +17 -1
- ccproxy/observability/metrics.py +1 -3
- ccproxy/observability/pushgateway.py +0 -2
- ccproxy/observability/stats_printer.py +2 -4
- ccproxy/observability/storage/duckdb_simple.py +1 -1
- ccproxy/observability/storage/models.py +0 -1
- ccproxy/pricing/cache.py +0 -1
- ccproxy/pricing/loader.py +5 -21
- ccproxy/pricing/updater.py +0 -1
- ccproxy/scheduler/__init__.py +1 -0
- ccproxy/scheduler/core.py +6 -6
- ccproxy/scheduler/manager.py +35 -7
- ccproxy/scheduler/registry.py +1 -1
- ccproxy/scheduler/tasks.py +127 -2
- ccproxy/services/claude_sdk_service.py +220 -328
- ccproxy/services/credentials/manager.py +0 -1
- ccproxy/services/credentials/oauth_client.py +1 -2
- ccproxy/services/proxy_service.py +93 -222
- ccproxy/testing/config.py +1 -1
- ccproxy/testing/mock_responses.py +0 -1
- ccproxy/utils/model_mapping.py +197 -0
- ccproxy/utils/models_provider.py +150 -0
- ccproxy/utils/simple_request_logger.py +284 -0
- ccproxy/utils/version_checker.py +184 -0
- {ccproxy_api-0.1.2.dist-info → ccproxy_api-0.1.3.dist-info}/METADATA +63 -2
- ccproxy_api-0.1.3.dist-info/RECORD +166 -0
- ccproxy/cli/commands/permission.py +0 -128
- ccproxy_api-0.1.2.dist-info/RECORD +0 -150
- /ccproxy/scheduler/{exceptions.py → errors.py} +0 -0
- {ccproxy_api-0.1.2.dist-info → ccproxy_api-0.1.3.dist-info}/WHEEL +0 -0
- {ccproxy_api-0.1.2.dist-info → ccproxy_api-0.1.3.dist-info}/entry_points.txt +0 -0
- {ccproxy_api-0.1.2.dist-info → ccproxy_api-0.1.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -8,19 +8,19 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
import json
|
|
10
10
|
import time
|
|
11
|
-
from collections.abc import
|
|
12
|
-
from typing import Any
|
|
11
|
+
from collections.abc import AsyncIterator
|
|
12
|
+
from typing import Any, Literal
|
|
13
|
+
|
|
14
|
+
import structlog
|
|
13
15
|
|
|
14
16
|
from .models import (
|
|
15
|
-
OpenAIStreamingChatCompletionResponse,
|
|
16
|
-
OpenAIStreamingChoice,
|
|
17
|
-
OpenAIStreamingDelta,
|
|
18
|
-
OpenAIUsage,
|
|
19
17
|
generate_openai_response_id,
|
|
20
|
-
generate_openai_system_fingerprint,
|
|
21
18
|
)
|
|
22
19
|
|
|
23
20
|
|
|
21
|
+
logger = structlog.get_logger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
24
|
class OpenAISSEFormatter:
|
|
25
25
|
"""Formats streaming responses to match OpenAI's SSE format."""
|
|
26
26
|
|
|
@@ -246,8 +246,7 @@ class OpenAIStreamProcessor:
|
|
|
246
246
|
created: int | None = None,
|
|
247
247
|
enable_usage: bool = True,
|
|
248
248
|
enable_tool_calls: bool = True,
|
|
249
|
-
|
|
250
|
-
chunk_size_words: int = 3,
|
|
249
|
+
output_format: Literal["sse", "dict"] = "sse",
|
|
251
250
|
):
|
|
252
251
|
"""Initialize the stream processor.
|
|
253
252
|
|
|
@@ -257,16 +256,14 @@ class OpenAIStreamProcessor:
|
|
|
257
256
|
created: Creation timestamp, current time if not provided
|
|
258
257
|
enable_usage: Whether to include usage information
|
|
259
258
|
enable_tool_calls: Whether to process tool calls
|
|
260
|
-
|
|
261
|
-
chunk_size_words: Number of words per text chunk
|
|
259
|
+
output_format: Output format - "sse" for Server-Sent Events strings, "dict" for dict objects
|
|
262
260
|
"""
|
|
263
261
|
self.message_id = message_id or generate_openai_response_id()
|
|
264
262
|
self.model = model
|
|
265
263
|
self.created = created or int(time.time())
|
|
266
264
|
self.enable_usage = enable_usage
|
|
267
265
|
self.enable_tool_calls = enable_tool_calls
|
|
268
|
-
self.
|
|
269
|
-
self.chunk_size_words = chunk_size_words
|
|
266
|
+
self.output_format = output_format
|
|
270
267
|
self.formatter = OpenAISSEFormatter()
|
|
271
268
|
|
|
272
269
|
# State tracking
|
|
@@ -281,71 +278,164 @@ class OpenAIStreamProcessor:
|
|
|
281
278
|
|
|
282
279
|
async def process_stream(
|
|
283
280
|
self, claude_stream: AsyncIterator[dict[str, Any]]
|
|
284
|
-
) -> AsyncIterator[str]:
|
|
281
|
+
) -> AsyncIterator[str | dict[str, Any]]:
|
|
285
282
|
"""Process a Claude/Anthropic stream into OpenAI format.
|
|
286
283
|
|
|
287
284
|
Args:
|
|
288
285
|
claude_stream: Async iterator of Claude response chunks
|
|
289
286
|
|
|
290
287
|
Yields:
|
|
291
|
-
OpenAI-formatted SSE strings
|
|
288
|
+
OpenAI-formatted SSE strings or dict objects based on output_format
|
|
292
289
|
"""
|
|
293
290
|
try:
|
|
291
|
+
chunk_count = 0
|
|
292
|
+
processed_count = 0
|
|
294
293
|
async for chunk in claude_stream:
|
|
294
|
+
chunk_count += 1
|
|
295
|
+
logger.debug(
|
|
296
|
+
"openai_stream_chunk_received",
|
|
297
|
+
chunk_count=chunk_count,
|
|
298
|
+
chunk_type=chunk.get("type"),
|
|
299
|
+
chunk=chunk,
|
|
300
|
+
)
|
|
295
301
|
async for sse_chunk in self._process_chunk(chunk):
|
|
302
|
+
processed_count += 1
|
|
303
|
+
logger.debug(
|
|
304
|
+
"openai_stream_chunk_processed",
|
|
305
|
+
processed_count=processed_count,
|
|
306
|
+
sse_chunk=sse_chunk,
|
|
307
|
+
)
|
|
296
308
|
yield sse_chunk
|
|
297
309
|
|
|
310
|
+
logger.debug(
|
|
311
|
+
"openai_stream_complete",
|
|
312
|
+
total_chunks=chunk_count,
|
|
313
|
+
processed_chunks=processed_count,
|
|
314
|
+
usage_info=self.usage_info,
|
|
315
|
+
)
|
|
316
|
+
|
|
298
317
|
# Send final chunk
|
|
299
318
|
if self.usage_info and self.enable_usage:
|
|
300
|
-
yield self.
|
|
301
|
-
self.message_id,
|
|
302
|
-
self.model,
|
|
303
|
-
self.created,
|
|
319
|
+
yield self._format_chunk_output(
|
|
304
320
|
finish_reason="stop",
|
|
305
321
|
usage=self.usage_info,
|
|
306
322
|
)
|
|
307
323
|
else:
|
|
308
|
-
yield self.
|
|
309
|
-
self.message_id, self.model, self.created, finish_reason="stop"
|
|
310
|
-
)
|
|
324
|
+
yield self._format_chunk_output(finish_reason="stop")
|
|
311
325
|
|
|
312
|
-
# Send DONE event
|
|
313
|
-
|
|
326
|
+
# Send DONE event (only for SSE format)
|
|
327
|
+
if self.output_format == "sse":
|
|
328
|
+
yield self.formatter.format_done()
|
|
314
329
|
|
|
315
330
|
except Exception as e:
|
|
316
331
|
# Send error chunk
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
332
|
+
if self.output_format == "sse":
|
|
333
|
+
yield self.formatter.format_error_chunk(
|
|
334
|
+
self.message_id, self.model, self.created, "error", str(e)
|
|
335
|
+
)
|
|
336
|
+
yield self.formatter.format_done()
|
|
337
|
+
else:
|
|
338
|
+
# Dict format error
|
|
339
|
+
yield self._create_chunk_dict(finish_reason="error")
|
|
321
340
|
|
|
322
|
-
async def _process_chunk(
|
|
341
|
+
async def _process_chunk(
|
|
342
|
+
self, chunk: dict[str, Any]
|
|
343
|
+
) -> AsyncIterator[str | dict[str, Any]]:
|
|
323
344
|
"""Process a single chunk from the Claude stream.
|
|
324
345
|
|
|
325
346
|
Args:
|
|
326
347
|
chunk: Claude response chunk
|
|
327
348
|
|
|
328
349
|
Yields:
|
|
329
|
-
OpenAI-formatted SSE strings
|
|
350
|
+
OpenAI-formatted SSE strings or dict objects based on output_format
|
|
330
351
|
"""
|
|
331
|
-
|
|
352
|
+
# Handle both Claude SDK and standard Anthropic API formats:
|
|
353
|
+
# Claude SDK format: {"event": "...", "data": {"type": "..."}}
|
|
354
|
+
# Anthropic API format: {"type": "...", ...}
|
|
355
|
+
event_type = chunk.get("event")
|
|
356
|
+
if event_type:
|
|
357
|
+
# Claude SDK format
|
|
358
|
+
chunk_data = chunk.get("data", {})
|
|
359
|
+
chunk_type = chunk_data.get("type")
|
|
360
|
+
else:
|
|
361
|
+
# Standard Anthropic API format
|
|
362
|
+
chunk_data = chunk
|
|
363
|
+
chunk_type = chunk.get("type")
|
|
332
364
|
|
|
333
365
|
if chunk_type == "message_start":
|
|
334
366
|
# Send initial role chunk
|
|
335
367
|
if not self.role_sent:
|
|
336
|
-
yield self.
|
|
337
|
-
self.message_id, self.model, self.created
|
|
338
|
-
)
|
|
368
|
+
yield self._format_chunk_output(delta={"role": "assistant"})
|
|
339
369
|
self.role_sent = True
|
|
340
370
|
|
|
341
371
|
elif chunk_type == "content_block_start":
|
|
342
|
-
block =
|
|
372
|
+
block = chunk_data.get("content_block", {})
|
|
343
373
|
if block.get("type") == "thinking":
|
|
344
374
|
# Start of thinking block
|
|
345
375
|
self.thinking_block_active = True
|
|
346
376
|
self.current_thinking_text = ""
|
|
347
377
|
self.current_thinking_signature = None
|
|
348
|
-
elif block.get("type") == "
|
|
378
|
+
elif block.get("type") == "system_message":
|
|
379
|
+
# Handle system message content block
|
|
380
|
+
system_text = block.get("text", "")
|
|
381
|
+
source = block.get("source", "claude_code_sdk")
|
|
382
|
+
# Format as text with clear source attribution
|
|
383
|
+
formatted_text = f"[{source}]: {system_text}"
|
|
384
|
+
yield self._format_chunk_output(delta={"content": formatted_text})
|
|
385
|
+
elif block.get("type") == "tool_use_sdk" and self.enable_tool_calls:
|
|
386
|
+
# Handle custom tool_use_sdk content block
|
|
387
|
+
tool_id = block.get("id", "")
|
|
388
|
+
tool_name = block.get("name", "")
|
|
389
|
+
tool_input = block.get("input", {})
|
|
390
|
+
source = block.get("source", "claude_code_sdk")
|
|
391
|
+
|
|
392
|
+
# For dict format, immediately yield the tool call
|
|
393
|
+
if self.output_format == "dict":
|
|
394
|
+
yield self._format_chunk_output(
|
|
395
|
+
delta={
|
|
396
|
+
"tool_calls": [
|
|
397
|
+
{
|
|
398
|
+
"index": 0,
|
|
399
|
+
"id": tool_id,
|
|
400
|
+
"type": "function",
|
|
401
|
+
"function": {
|
|
402
|
+
"name": tool_name,
|
|
403
|
+
"arguments": json.dumps(tool_input),
|
|
404
|
+
},
|
|
405
|
+
}
|
|
406
|
+
]
|
|
407
|
+
}
|
|
408
|
+
)
|
|
409
|
+
else:
|
|
410
|
+
# For SSE format, store for later processing
|
|
411
|
+
self.tool_calls[tool_id] = {
|
|
412
|
+
"id": tool_id,
|
|
413
|
+
"name": tool_name,
|
|
414
|
+
"arguments": tool_input,
|
|
415
|
+
"source": source,
|
|
416
|
+
}
|
|
417
|
+
elif block.get("type") == "tool_result_sdk":
|
|
418
|
+
# Handle custom tool_result_sdk content block
|
|
419
|
+
source = block.get("source", "claude_code_sdk")
|
|
420
|
+
tool_use_id = block.get("tool_use_id", "")
|
|
421
|
+
result_content = block.get("content", "")
|
|
422
|
+
is_error = block.get("is_error", False)
|
|
423
|
+
error_indicator = " (ERROR)" if is_error else ""
|
|
424
|
+
formatted_text = f"[{source} tool_result {tool_use_id}{error_indicator}]: {result_content}"
|
|
425
|
+
yield self._format_chunk_output(delta={"content": formatted_text})
|
|
426
|
+
elif block.get("type") == "result_message":
|
|
427
|
+
# Handle custom result_message content block
|
|
428
|
+
source = block.get("source", "claude_code_sdk")
|
|
429
|
+
result_data = block.get("data", {})
|
|
430
|
+
session_id = result_data.get("session_id", "")
|
|
431
|
+
stop_reason = result_data.get("stop_reason", "")
|
|
432
|
+
usage = result_data.get("usage", {})
|
|
433
|
+
cost_usd = result_data.get("total_cost_usd")
|
|
434
|
+
formatted_text = f"[{source} result {session_id}]: stop_reason={stop_reason}, usage={usage}"
|
|
435
|
+
if cost_usd is not None:
|
|
436
|
+
formatted_text += f", cost_usd={cost_usd}"
|
|
437
|
+
yield self._format_chunk_output(delta={"content": formatted_text})
|
|
438
|
+
elif block.get("type") == "tool_use":
|
|
349
439
|
# Start of tool call
|
|
350
440
|
tool_id = block.get("id", "")
|
|
351
441
|
tool_name = block.get("name", "")
|
|
@@ -356,27 +446,14 @@ class OpenAIStreamProcessor:
|
|
|
356
446
|
}
|
|
357
447
|
|
|
358
448
|
elif chunk_type == "content_block_delta":
|
|
359
|
-
delta =
|
|
449
|
+
delta = chunk_data.get("delta", {})
|
|
360
450
|
delta_type = delta.get("type")
|
|
361
451
|
|
|
362
452
|
if delta_type == "text_delta":
|
|
363
453
|
# Text content
|
|
364
454
|
text = delta.get("text", "")
|
|
365
455
|
if text:
|
|
366
|
-
|
|
367
|
-
# Chunk the text
|
|
368
|
-
words = text.split()
|
|
369
|
-
for i in range(0, len(words), self.chunk_size_words):
|
|
370
|
-
chunk_words = words[i : i + self.chunk_size_words]
|
|
371
|
-
chunk_text = " ".join(chunk_words)
|
|
372
|
-
yield self.formatter.format_content_chunk(
|
|
373
|
-
self.message_id, self.model, self.created, chunk_text
|
|
374
|
-
)
|
|
375
|
-
else:
|
|
376
|
-
# Send text as-is
|
|
377
|
-
yield self.formatter.format_content_chunk(
|
|
378
|
-
self.message_id, self.model, self.created, text
|
|
379
|
-
)
|
|
456
|
+
yield self._format_chunk_output(delta={"content": text})
|
|
380
457
|
|
|
381
458
|
elif delta_type == "thinking_delta" and self.thinking_block_active:
|
|
382
459
|
# Thinking content
|
|
@@ -392,7 +469,7 @@ class OpenAIStreamProcessor:
|
|
|
392
469
|
self.current_thinking_signature = ""
|
|
393
470
|
self.current_thinking_signature += signature
|
|
394
471
|
|
|
395
|
-
elif delta_type == "input_json_delta"
|
|
472
|
+
elif delta_type == "input_json_delta":
|
|
396
473
|
# Tool call arguments
|
|
397
474
|
partial_json = delta.get("partial_json", "")
|
|
398
475
|
if partial_json and self.tool_calls:
|
|
@@ -408,28 +485,39 @@ class OpenAIStreamProcessor:
|
|
|
408
485
|
if self.current_thinking_text:
|
|
409
486
|
# Format thinking block with signature
|
|
410
487
|
thinking_content = f'<thinking signature="{self.current_thinking_signature}">{self.current_thinking_text}</thinking>'
|
|
411
|
-
yield self.
|
|
412
|
-
self.message_id, self.model, self.created, thinking_content
|
|
413
|
-
)
|
|
488
|
+
yield self._format_chunk_output(delta={"content": thinking_content})
|
|
414
489
|
# Reset thinking state
|
|
415
490
|
self.current_thinking_text = ""
|
|
416
491
|
self.current_thinking_signature = None
|
|
417
492
|
|
|
418
|
-
elif
|
|
419
|
-
|
|
493
|
+
elif (
|
|
494
|
+
self.tool_calls
|
|
495
|
+
and self.enable_tool_calls
|
|
496
|
+
and self.output_format == "sse"
|
|
497
|
+
):
|
|
498
|
+
# Send completed tool calls (only for SSE format, dict format sends immediately)
|
|
420
499
|
for tool_call in self.tool_calls.values():
|
|
421
|
-
yield self.
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
500
|
+
yield self._format_chunk_output(
|
|
501
|
+
delta={
|
|
502
|
+
"tool_calls": [
|
|
503
|
+
{
|
|
504
|
+
"index": 0,
|
|
505
|
+
"id": tool_call["id"],
|
|
506
|
+
"type": "function",
|
|
507
|
+
"function": {
|
|
508
|
+
"name": tool_call["name"],
|
|
509
|
+
"arguments": json.dumps(tool_call["arguments"])
|
|
510
|
+
if isinstance(tool_call["arguments"], dict)
|
|
511
|
+
else tool_call["arguments"],
|
|
512
|
+
},
|
|
513
|
+
}
|
|
514
|
+
]
|
|
515
|
+
}
|
|
428
516
|
)
|
|
429
517
|
|
|
430
518
|
elif chunk_type == "message_delta":
|
|
431
519
|
# Usage information
|
|
432
|
-
usage =
|
|
520
|
+
usage = chunk_data.get("usage", {})
|
|
433
521
|
if usage and self.enable_usage:
|
|
434
522
|
self.usage_info = {
|
|
435
523
|
"prompt_tokens": usage.get("input_tokens", 0),
|
|
@@ -442,6 +530,100 @@ class OpenAIStreamProcessor:
|
|
|
442
530
|
# End of message - handled in main process_stream method
|
|
443
531
|
pass
|
|
444
532
|
|
|
533
|
+
def _create_chunk_dict(
|
|
534
|
+
self,
|
|
535
|
+
delta: dict[str, Any] | None = None,
|
|
536
|
+
finish_reason: str | None = None,
|
|
537
|
+
usage: dict[str, int] | None = None,
|
|
538
|
+
) -> dict[str, Any]:
|
|
539
|
+
"""Create an OpenAI completion chunk dict.
|
|
540
|
+
|
|
541
|
+
Args:
|
|
542
|
+
delta: The delta content for the chunk
|
|
543
|
+
finish_reason: Optional finish reason
|
|
544
|
+
usage: Optional usage information
|
|
545
|
+
|
|
546
|
+
Returns:
|
|
547
|
+
OpenAI completion chunk dict
|
|
548
|
+
"""
|
|
549
|
+
chunk = {
|
|
550
|
+
"id": self.message_id,
|
|
551
|
+
"object": "chat.completion.chunk",
|
|
552
|
+
"created": self.created,
|
|
553
|
+
"model": self.model,
|
|
554
|
+
"choices": [
|
|
555
|
+
{
|
|
556
|
+
"index": 0,
|
|
557
|
+
"delta": delta or {},
|
|
558
|
+
"logprobs": None,
|
|
559
|
+
"finish_reason": finish_reason,
|
|
560
|
+
}
|
|
561
|
+
],
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if usage:
|
|
565
|
+
chunk["usage"] = usage
|
|
566
|
+
|
|
567
|
+
return chunk
|
|
568
|
+
|
|
569
|
+
def _format_chunk_output(
|
|
570
|
+
self,
|
|
571
|
+
delta: dict[str, Any] | None = None,
|
|
572
|
+
finish_reason: str | None = None,
|
|
573
|
+
usage: dict[str, int] | None = None,
|
|
574
|
+
) -> str | dict[str, Any]:
|
|
575
|
+
"""Format chunk output based on output_format flag.
|
|
576
|
+
|
|
577
|
+
Args:
|
|
578
|
+
delta: The delta content for the chunk
|
|
579
|
+
finish_reason: Optional finish reason
|
|
580
|
+
usage: Optional usage information
|
|
581
|
+
|
|
582
|
+
Returns:
|
|
583
|
+
Either SSE string or dict based on output_format
|
|
584
|
+
"""
|
|
585
|
+
if self.output_format == "dict":
|
|
586
|
+
return self._create_chunk_dict(delta, finish_reason, usage)
|
|
587
|
+
else:
|
|
588
|
+
# SSE format
|
|
589
|
+
if finish_reason:
|
|
590
|
+
if usage:
|
|
591
|
+
return self.formatter.format_final_chunk(
|
|
592
|
+
self.message_id,
|
|
593
|
+
self.model,
|
|
594
|
+
self.created,
|
|
595
|
+
finish_reason,
|
|
596
|
+
usage=usage,
|
|
597
|
+
)
|
|
598
|
+
else:
|
|
599
|
+
return self.formatter.format_final_chunk(
|
|
600
|
+
self.message_id, self.model, self.created, finish_reason
|
|
601
|
+
)
|
|
602
|
+
elif delta and delta.get("role"):
|
|
603
|
+
return self.formatter.format_first_chunk(
|
|
604
|
+
self.message_id, self.model, self.created, delta["role"]
|
|
605
|
+
)
|
|
606
|
+
elif delta and delta.get("content"):
|
|
607
|
+
return self.formatter.format_content_chunk(
|
|
608
|
+
self.message_id, self.model, self.created, delta["content"]
|
|
609
|
+
)
|
|
610
|
+
elif delta and delta.get("tool_calls"):
|
|
611
|
+
# Handle tool calls
|
|
612
|
+
tool_call = delta["tool_calls"][0] # Assume single tool call for now
|
|
613
|
+
return self.formatter.format_tool_call_chunk(
|
|
614
|
+
self.message_id,
|
|
615
|
+
self.model,
|
|
616
|
+
self.created,
|
|
617
|
+
tool_call["id"],
|
|
618
|
+
tool_call.get("function", {}).get("name"),
|
|
619
|
+
tool_call.get("function", {}).get("arguments"),
|
|
620
|
+
)
|
|
621
|
+
else:
|
|
622
|
+
# Empty delta
|
|
623
|
+
return self.formatter.format_final_chunk(
|
|
624
|
+
self.message_id, self.model, self.created, "stop"
|
|
625
|
+
)
|
|
626
|
+
|
|
445
627
|
|
|
446
628
|
__all__ = [
|
|
447
629
|
"OpenAISSEFormatter",
|