coze-coding-utils 0.2.3a3__tar.gz → 0.2.5__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 (39) hide show
  1. {coze_coding_utils-0.2.3a3 → coze_coding_utils-0.2.5}/PKG-INFO +1 -1
  2. {coze_coding_utils-0.2.3a3 → coze_coding_utils-0.2.5}/pyproject.toml +1 -1
  3. {coze_coding_utils-0.2.3a3 → coze_coding_utils-0.2.5}/src/coze_coding_utils/helper/agent_helper.py +93 -15
  4. {coze_coding_utils-0.2.3a3 → coze_coding_utils-0.2.5}/src/coze_coding_utils/helper/stream_runner.py +31 -22
  5. {coze_coding_utils-0.2.3a3 → coze_coding_utils-0.2.5}/src/coze_coding_utils/log/node_log.py +0 -1
  6. {coze_coding_utils-0.2.3a3 → coze_coding_utils-0.2.5}/src/coze_coding_utils/openai/converter/request_converter.py +22 -0
  7. coze_coding_utils-0.2.3a3/src/coze_coding_utils/error/test_classifier.py +0 -0
  8. {coze_coding_utils-0.2.3a3 → coze_coding_utils-0.2.5}/.gitignore +0 -0
  9. {coze_coding_utils-0.2.3a3 → coze_coding_utils-0.2.5}/LICENSE +0 -0
  10. {coze_coding_utils-0.2.3a3 → coze_coding_utils-0.2.5}/README.md +0 -0
  11. {coze_coding_utils-0.2.3a3 → coze_coding_utils-0.2.5}/src/coze_coding_utils/__init__.py +0 -0
  12. {coze_coding_utils-0.2.3a3 → coze_coding_utils-0.2.5}/src/coze_coding_utils/error/__init__.py +0 -0
  13. {coze_coding_utils-0.2.3a3 → coze_coding_utils-0.2.5}/src/coze_coding_utils/error/classifier.py +0 -0
  14. {coze_coding_utils-0.2.3a3 → coze_coding_utils-0.2.5}/src/coze_coding_utils/error/codes.py +0 -0
  15. {coze_coding_utils-0.2.3a3 → coze_coding_utils-0.2.5}/src/coze_coding_utils/error/exceptions.py +0 -0
  16. {coze_coding_utils-0.2.3a3 → coze_coding_utils-0.2.5}/src/coze_coding_utils/error/patterns.py +0 -0
  17. {coze_coding_utils-0.2.3a3 → coze_coding_utils-0.2.5}/src/coze_coding_utils/file/__init__.py +0 -0
  18. {coze_coding_utils-0.2.3a3 → coze_coding_utils-0.2.5}/src/coze_coding_utils/file/file.py +0 -0
  19. {coze_coding_utils-0.2.3a3 → coze_coding_utils-0.2.5}/src/coze_coding_utils/helper/__init__.py +0 -0
  20. {coze_coding_utils-0.2.3a3 → coze_coding_utils-0.2.5}/src/coze_coding_utils/helper/graph_helper.py +0 -0
  21. {coze_coding_utils-0.2.3a3 → coze_coding_utils-0.2.5}/src/coze_coding_utils/log/__init__.py +0 -0
  22. {coze_coding_utils-0.2.3a3 → coze_coding_utils-0.2.5}/src/coze_coding_utils/log/common.py +0 -0
  23. {coze_coding_utils-0.2.3a3 → coze_coding_utils-0.2.5}/src/coze_coding_utils/log/config.py +0 -0
  24. {coze_coding_utils-0.2.3a3 → coze_coding_utils-0.2.5}/src/coze_coding_utils/log/err_trace.py +0 -0
  25. {coze_coding_utils-0.2.3a3 → coze_coding_utils-0.2.5}/src/coze_coding_utils/log/loop_trace.py +0 -0
  26. {coze_coding_utils-0.2.3a3 → coze_coding_utils-0.2.5}/src/coze_coding_utils/log/parser.py +0 -0
  27. {coze_coding_utils-0.2.3a3 → coze_coding_utils-0.2.5}/src/coze_coding_utils/log/write_log.py +0 -0
  28. {coze_coding_utils-0.2.3a3 → coze_coding_utils-0.2.5}/src/coze_coding_utils/messages/__init__.py +0 -0
  29. {coze_coding_utils-0.2.3a3 → coze_coding_utils-0.2.5}/src/coze_coding_utils/messages/client.py +0 -0
  30. {coze_coding_utils-0.2.3a3 → coze_coding_utils-0.2.5}/src/coze_coding_utils/messages/server.py +0 -0
  31. {coze_coding_utils-0.2.3a3 → coze_coding_utils-0.2.5}/src/coze_coding_utils/openai/__init__.py +0 -0
  32. {coze_coding_utils-0.2.3a3 → coze_coding_utils-0.2.5}/src/coze_coding_utils/openai/converter/__init__.py +0 -0
  33. {coze_coding_utils-0.2.3a3 → coze_coding_utils-0.2.5}/src/coze_coding_utils/openai/converter/response_converter.py +0 -0
  34. {coze_coding_utils-0.2.3a3 → coze_coding_utils-0.2.5}/src/coze_coding_utils/openai/handler.py +0 -0
  35. {coze_coding_utils-0.2.3a3 → coze_coding_utils-0.2.5}/src/coze_coding_utils/openai/types/__init__.py +0 -0
  36. {coze_coding_utils-0.2.3a3 → coze_coding_utils-0.2.5}/src/coze_coding_utils/openai/types/request.py +0 -0
  37. {coze_coding_utils-0.2.3a3 → coze_coding_utils-0.2.5}/src/coze_coding_utils/openai/types/response.py +0 -0
  38. {coze_coding_utils-0.2.3a3 → coze_coding_utils-0.2.5}/src/coze_coding_utils/runtime_ctx/__init__.py +0 -0
  39. {coze_coding_utils-0.2.3a3 → coze_coding_utils-0.2.5}/src/coze_coding_utils/runtime_ctx/context.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coze-coding-utils
3
- Version: 0.2.3a3
3
+ Version: 0.2.5
4
4
  Summary: Utilities for Coze coding client runtime context and helpers.
5
5
  Project-URL: Homepage, https://code.byted.org/stone/coze-coding-client
6
6
  Author: Bytedance Stone Team
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "coze-coding-utils"
7
- version = "0.2.3a3"
7
+ version = "0.2.5"
8
8
  description = "Utilities for Coze coding client runtime context and helpers."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,7 +1,8 @@
1
1
  import uuid
2
2
  import json
3
- import os
4
- from typing import Any, Dict, List, Tuple, Iterator
3
+ import requests
4
+ from urllib.parse import urlparse, urlencode, parse_qs, urljoin
5
+ from typing import Any, Dict, List, Tuple, Iterator, Optional
5
6
  import time
6
7
  from coze_coding_utils.file.file import File, FileOps, infer_file_category
7
8
  from coze_coding_utils.error import classify_error
@@ -31,6 +32,78 @@ from coze_coding_utils.messages.server import (
31
32
  )
32
33
 
33
34
 
35
+ _VIDEO_MIME_MAP: Dict[str, str] = {
36
+ "mp4": "video/mp4",
37
+ "avi": "video/x-msvideo",
38
+ "mov": "video/quicktime",
39
+ "mkv": "video/x-matroska",
40
+ "flv": "video/x-flv",
41
+ "wmv": "video/x-ms-wmv",
42
+ "webm": "video/webm",
43
+ "m4v": "video/x-m4v",
44
+ "3gp": "video/3gpp",
45
+ }
46
+
47
+ # 下游 LLM API 实际支持的视频 MIME 类型白名单
48
+ _SUPPORTED_VIDEO_MIMES = {
49
+ "video/mp4",
50
+ "video/quicktime",
51
+ "video/webm",
52
+ "video/3gpp",
53
+ "video/x-m4v",
54
+ }
55
+
56
+
57
+ def _probe_remote_mime(url: str) -> Optional[str]:
58
+ """HEAD 请求探测远程文件的真实 Content-Type,失败时返回 None"""
59
+ try:
60
+ resp = requests.head(url, timeout=5, allow_redirects=True)
61
+ ct = resp.headers.get("Content-Type", "")
62
+ return ct.split(";")[0].strip() or None
63
+ except Exception:
64
+ return None
65
+
66
+
67
+ def _ensure_video_content_type(url: str, mime_type: str) -> str:
68
+ """
69
+ 对象存储(TOS/S3)的 presigned URL 可能返回 application/octet-stream。
70
+ 通过追加 response-content-type 参数让服务端在响应中覆盖 Content-Type,
71
+ 从而使 LLM API fetch URL 时能拿到正确的 video MIME,而不会因 octet-stream 被拒。
72
+ 已有签名不受影响(TOS/S3 response-* 参数不计入签名哈希)。
73
+ """
74
+ parsed = urlparse(url)
75
+ # 已经有正确的 response-content-type,不重复添加
76
+ qs = parse_qs(parsed.query, keep_blank_values=True)
77
+ if qs.get("response-content-type") == [mime_type]:
78
+ return url
79
+ sep = "&" if parsed.query else "?"
80
+ return url + sep + "response-content-type=" + requests.utils.quote(mime_type, safe="")
81
+
82
+
83
+ def _build_video_parts(
84
+ file_data: File, file_info: Any, file_ext: str, mime_override: Optional[str] = None
85
+ ) -> List[Dict[str, Any]]:
86
+ """将视频文件转换为 content_parts,不支持的格式退化为文本描述。
87
+ mime_override: 外部已探测好的 MIME,传入时跳过内部探测,避免重复 HEAD 请求。
88
+ """
89
+ ext = file_ext.lstrip(".").lower()
90
+ mime_type = mime_override or _VIDEO_MIME_MAP.get(ext)
91
+ if not mime_type and file_data.is_remote:
92
+ mime_type = _probe_remote_mime(file_info.url)
93
+
94
+ if mime_type and mime_type in _SUPPORTED_VIDEO_MIMES:
95
+ # 追加 response-content-type 参数,确保对象存储返回正确的 Content-Type
96
+ # 防止 LLM API fetch URL 时因拿到 application/octet-stream 而报错
97
+ video_url = _ensure_video_content_type(file_info.url, mime_type) if file_data.is_remote else file_info.url
98
+ return [
99
+ {"type": "text", "text": f'{file_data.url}'},
100
+ {"type": "video_url", "video_url": {"url": video_url, "media_type": mime_type}},
101
+ ]
102
+ else:
103
+ fmt_hint = mime_type or (f".{ext}" if ext else "unknown")
104
+ return [{"type": "text", "text": f"video url: {file_info.url} (format: {fmt_hint})"}]
105
+
106
+
34
107
  def to_stream_input(msg: ClientMessage) -> Dict[str, Any]:
35
108
  content_parts = []
36
109
  if msg and msg.content and msg.content.query and msg.content.query.prompt:
@@ -43,7 +116,23 @@ def to_stream_input(msg: ClientMessage) -> Dict[str, Any]:
43
116
  and block.content.upload_file
44
117
  ):
45
118
  file_info = block.content.upload_file
46
- file_type, _ = infer_file_category(file_info.url)
119
+ # 第一轮:从 URL 后缀识别类型
120
+ file_type, file_ext = infer_file_category(file_info.url)
121
+ # 第二轮:URL 无后缀时用 file_name 兜底
122
+ if file_type == "default" and file_info.file_name:
123
+ file_type, file_ext = infer_file_category(file_info.file_name)
124
+ # 第三轮:两轮扩展名都无法识别时,HEAD 探测真实 Content-Type
125
+ probed_mime: Optional[str] = None
126
+ if file_type == "default" and file_info.url.startswith(("http://", "https://")):
127
+ probed_mime = _probe_remote_mime(file_info.url)
128
+ if probed_mime:
129
+ top = probed_mime.split("/")[0]
130
+ if top == "video":
131
+ file_type = "video"
132
+ elif top == "image":
133
+ file_type = "image"
134
+ elif top == "audio":
135
+ file_type = "audio"
47
136
  file_data = File(url=file_info.url, file_type=file_type)
48
137
  # check is image
49
138
  if file_data.file_type == "image":
@@ -61,18 +150,7 @@ def to_stream_input(msg: ClientMessage) -> Dict[str, Any]:
61
150
  )
62
151
  # check is video
63
152
  elif file_data.file_type == "video":
64
- content_parts.append(
65
- {
66
- "type": "text",
67
- "text": f'{file_data.url}'
68
- }
69
- )
70
- content_parts.append(
71
- {
72
- "type": "video_url",
73
- "video_url": {"url": file_info.url},
74
- }
75
- )
153
+ content_parts.extend(_build_video_parts(file_data, file_info, file_ext, probed_mime))
76
154
  # check is audio
77
155
  elif file_data.file_type == "audio":
78
156
  content_parts.append(
@@ -4,7 +4,8 @@ import threading
4
4
  import contextvars
5
5
  import logging
6
6
  from abc import ABC, abstractmethod
7
- from typing import Any, Dict, Iterator, AsyncIterable
7
+ from dataclasses import dataclass
8
+ from typing import Any, Dict, Iterator, AsyncIterable, Optional, Literal
8
9
  from langchain_core.runnables import RunnableConfig
9
10
  from langgraph.graph.state import CompiledStateGraph
10
11
  from coze_coding_utils.helper.agent_helper import (
@@ -31,6 +32,15 @@ logger = logging.getLogger(__name__)
31
32
  TIMEOUT_SECONDS = 900
32
33
  PING_INTERVAL_SECONDS = 30
33
34
 
35
+
36
+ @dataclass
37
+ class RunOpt:
38
+ workflow_debug: bool = False
39
+
40
+ @property
41
+ def stream_mode(self) -> Literal["debug", "updates"]:
42
+ return "debug" if self.workflow_debug else "updates"
43
+
34
44
  class WorkflowEventType:
35
45
  WORKFLOW_START = "workflow_start"
36
46
  WORKFLOW_END = "workflow_end"
@@ -47,16 +57,16 @@ class WorkflowErrorCode:
47
57
 
48
58
  class BaseStreamRunner(ABC):
49
59
  @abstractmethod
50
- def stream(self, payload: Dict[str, Any], graph: CompiledStateGraph, run_config: RunnableConfig, ctx: Context) -> Iterator[Any]:
60
+ def stream(self, payload: Dict[str, Any], graph: CompiledStateGraph, run_config: RunnableConfig, ctx: Context, run_opt: Optional[RunOpt] = None) -> Iterator[Any]:
51
61
  pass
52
62
 
53
63
  @abstractmethod
54
- async def astream(self, payload: Dict[str, Any], graph: CompiledStateGraph, run_config: RunnableConfig, ctx: Context) -> AsyncIterable[Any]:
64
+ async def astream(self, payload: Dict[str, Any], graph: CompiledStateGraph, run_config: RunnableConfig, ctx: Context, run_opt: Optional[RunOpt] = None) -> AsyncIterable[Any]:
55
65
  pass
56
66
 
57
67
 
58
68
  class AgentStreamRunner(BaseStreamRunner):
59
- def stream(self, payload: Dict[str, Any], graph: CompiledStateGraph, run_config: RunnableConfig, ctx: Context) -> Iterator[Any]:
69
+ def stream(self, payload: Dict[str, Any], graph: CompiledStateGraph, run_config: RunnableConfig, ctx: Context, run_opt: Optional[RunOpt] = None) -> Iterator[Any]:
60
70
  client_msg, session_id = to_client_message(payload)
61
71
  run_config["recursion_limit"] = 100
62
72
  run_config["configurable"] = {"thread_id": session_id}
@@ -101,7 +111,7 @@ class AgentStreamRunner(BaseStreamRunner):
101
111
  )
102
112
  yield end_msg
103
113
 
104
- async def astream(self, payload: Dict[str, Any], graph: CompiledStateGraph, run_config: RunnableConfig, ctx: Context) -> AsyncIterable[Any]:
114
+ async def astream(self, payload: Dict[str, Any], graph: CompiledStateGraph, run_config: RunnableConfig, ctx: Context, run_opt: Optional[RunOpt] = None) -> AsyncIterable[Any]:
105
115
  client_msg, session_id = to_client_message(payload)
106
116
  run_config["recursion_limit"] = 100
107
117
  run_config["configurable"] = {"thread_id": session_id}
@@ -194,9 +204,6 @@ class AgentStreamRunner(BaseStreamRunner):
194
204
 
195
205
 
196
206
  class WorkflowStreamRunner(BaseStreamRunner):
197
- def __init__(self):
198
- self._node_start_times: Dict[str, float] = {}
199
-
200
207
  def _serialize_data(self, data: Any) -> Any:
201
208
  if isinstance(data, dict):
202
209
  return {k: self._serialize_data(v) for k, v in data.items()}
@@ -221,7 +228,9 @@ class WorkflowStreamRunner(BaseStreamRunner):
221
228
  result.update(kwargs)
222
229
  return result
223
230
 
224
- def stream(self, payload: Dict[str, Any], graph: CompiledStateGraph, run_config: RunnableConfig, ctx: Context) -> Iterator[Any]:
231
+ def stream(self, payload: Dict[str, Any], graph: CompiledStateGraph, run_config: RunnableConfig, ctx: Context, run_opt: Optional[RunOpt] = None) -> Iterator[Any]:
232
+ if run_opt is None:
233
+ run_opt = RunOpt()
225
234
  run_config["recursion_limit"] = 100
226
235
  if "configurable" not in run_config:
227
236
  run_config["configurable"] = {}
@@ -232,21 +241,19 @@ class WorkflowStreamRunner(BaseStreamRunner):
232
241
  node_start_times: Dict[str, float] = {}
233
242
  final_output = {}
234
243
  seq = 0
235
- is_debug = run_config.get("configurable", {}).get("workflow_debug", False)
236
- stream_mode = "debug" if is_debug else "updates"
237
244
 
238
245
  try:
239
246
  seq += 1
240
247
  yield (seq, self._build_event(WorkflowEventType.WORKFLOW_START, ctx))
241
248
 
242
- for event in graph.stream(payload, stream_mode=stream_mode, config=run_config, context=ctx):
249
+ for event in graph.stream(payload, stream_mode=run_opt.stream_mode, config=run_config, context=ctx):
243
250
  current_time = time.time()
244
251
  if current_time - last_ping_time >= PING_INTERVAL_SECONDS:
245
252
  seq += 1
246
253
  yield (seq, self._build_event(WorkflowEventType.PING, ctx))
247
254
  last_ping_time = current_time
248
255
 
249
- if not is_debug:
256
+ if not run_opt.workflow_debug:
250
257
  if isinstance(event, dict):
251
258
  logger.info(f"Debug event: {event}")
252
259
  for node_name, node_output in event.items():
@@ -311,7 +318,9 @@ class WorkflowStreamRunner(BaseStreamRunner):
311
318
  seq += 1
312
319
  yield (seq, self._build_event(WorkflowEventType.ERROR, ctx, code=str(err.code), error_msg=err.message))
313
320
 
314
- async def astream(self, payload: Dict[str, Any], graph: CompiledStateGraph, run_config: RunnableConfig, ctx: Context) -> AsyncIterable[Any]:
321
+ async def astream(self, payload: Dict[str, Any], graph: CompiledStateGraph, run_config: RunnableConfig, ctx: Context, run_opt: Optional[RunOpt] = None) -> AsyncIterable[Any]:
322
+ if run_opt is None:
323
+ run_opt = RunOpt()
315
324
  run_config["recursion_limit"] = 100
316
325
  if "configurable" not in run_config:
317
326
  run_config["configurable"] = {}
@@ -323,9 +332,7 @@ class WorkflowStreamRunner(BaseStreamRunner):
323
332
  start_time = time.time()
324
333
  cancelled = threading.Event()
325
334
  last_ping_time = [start_time]
326
- is_debug = run_config.get("configurable", {}).get("workflow_debug", False)
327
- stream_mode = "debug" if is_debug else "updates"
328
- logger.info(f"Stream mode: {stream_mode}")
335
+ logger.info(f"Stream mode: {run_opt.stream_mode}")
329
336
  seq = [0]
330
337
 
331
338
  def producer():
@@ -339,7 +346,7 @@ class WorkflowStreamRunner(BaseStreamRunner):
339
346
  seq[0] += 1
340
347
  loop.call_soon_threadsafe(q.put_nowait, (seq[0], self._build_event(WorkflowEventType.WORKFLOW_START, ctx)))
341
348
 
342
- for event in graph.stream(payload, stream_mode=stream_mode, config=run_config, context=ctx):
349
+ for event in graph.stream(payload, stream_mode=run_opt.stream_mode, config=run_config, context=ctx):
343
350
  if cancelled.is_set():
344
351
  logger.info(f"Workflow producer cancelled during iteration for run_id: {ctx.run_id}")
345
352
  seq[0] += 1
@@ -358,7 +365,7 @@ class WorkflowStreamRunner(BaseStreamRunner):
358
365
  loop.call_soon_threadsafe(q.put_nowait, (seq[0], self._build_event(WorkflowEventType.PING, ctx)))
359
366
  last_ping_time[0] = current_time
360
367
 
361
- if not is_debug:
368
+ if not run_opt.workflow_debug:
362
369
  if isinstance(event, dict):
363
370
  for node_name, node_output in event.items():
364
371
  logger.info(f"Node output: {node_name}")
@@ -474,7 +481,7 @@ async def agent_stream_handler(
474
481
  t0 = time.time()
475
482
 
476
483
  try:
477
- async for chunk in stream_sse_func(payload, ctx, need_detail=False):
484
+ async for chunk in stream_sse_func(payload, ctx):
478
485
  yield chunk
479
486
  except asyncio.CancelledError:
480
487
  logger.info(f"Agent stream cancelled for run_id: {run_id}")
@@ -517,15 +524,17 @@ async def workflow_stream_handler(
517
524
  sse_event_func: Callable,
518
525
  error_classifier: ErrorClassifier,
519
526
  register_task_func: Callable[[str, asyncio.Task], None],
520
- workflow_debug: bool = False,
527
+ run_opt: Optional[RunOpt] = None,
521
528
  ) -> AsyncGenerator[str, None]:
529
+ if run_opt is None:
530
+ run_opt = RunOpt()
522
531
  task = asyncio.current_task()
523
532
  if task:
524
533
  register_task_func(run_id, task)
525
534
  logger.info(f"Registered workflow streaming task for run_id: {run_id}")
526
535
 
527
536
  try:
528
- async for chunk in stream_sse_func(payload, ctx, need_detail=workflow_debug):
537
+ async for chunk in stream_sse_func(payload, ctx, run_opt):
529
538
  yield chunk
530
539
  except asyncio.CancelledError:
531
540
  logger.info(f"Workflow stream cancelled for run_id: {run_id}")
@@ -1,7 +1,6 @@
1
1
  import time
2
2
  import logging
3
3
  from uuid import UUID
4
- from openai import BaseModel
5
4
  from coze_coding_utils.log.config import LOG_DIR
6
5
  from coze_coding_utils.log.common import get_execute_mode, is_prod
7
6
  import uuid
@@ -6,6 +6,10 @@ from coze_coding_utils.openai.types.request import (
6
6
  ChatMessage,
7
7
  )
8
8
  from coze_coding_utils.file.file import File, FileOps, infer_file_category
9
+ from coze_coding_utils.helper.agent_helper import (
10
+ _VIDEO_MIME_MAP,
11
+ _ensure_video_content_type,
12
+ )
9
13
 
10
14
 
11
15
  class RequestConverter:
@@ -110,6 +114,15 @@ class RequestConverter:
110
114
  video_url_data = part.get("video_url", {})
111
115
  url = video_url_data.get("url", "")
112
116
  if url:
117
+ _, file_ext = infer_file_category(url)
118
+ ext = file_ext.lstrip(".").lower()
119
+ mime_type = _VIDEO_MIME_MAP.get(ext)
120
+ if mime_type:
121
+ fixed_url = _ensure_video_content_type(url, mime_type)
122
+ return [
123
+ {"type": "text", "text": url},
124
+ {"type": "video_url", "video_url": {"url": fixed_url, "media_type": mime_type}},
125
+ ]
113
126
  return [
114
127
  {"type": "text", "text": url},
115
128
  {"type": "video_url", "video_url": {"url": url}},
@@ -147,6 +160,15 @@ class RequestConverter:
147
160
  {"type": "image_url", "image_url": {"url": url}},
148
161
  ]
149
162
  elif file_type == "video":
163
+ _, file_ext = infer_file_category(url)
164
+ ext = file_ext.lstrip(".").lower()
165
+ mime_type = _VIDEO_MIME_MAP.get(ext)
166
+ if mime_type:
167
+ fixed_url = _ensure_video_content_type(url, mime_type)
168
+ return [
169
+ {"type": "text", "text": url},
170
+ {"type": "video_url", "video_url": {"url": fixed_url, "media_type": mime_type}},
171
+ ]
150
172
  return [
151
173
  {"type": "text", "text": url},
152
174
  {"type": "video_url", "video_url": {"url": url}},