nous-genai 0.1.0__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.
- nous/__init__.py +3 -0
- nous/genai/__init__.py +56 -0
- nous/genai/__main__.py +3 -0
- nous/genai/_internal/__init__.py +1 -0
- nous/genai/_internal/capability_rules.py +476 -0
- nous/genai/_internal/config.py +102 -0
- nous/genai/_internal/errors.py +63 -0
- nous/genai/_internal/http.py +951 -0
- nous/genai/_internal/json_schema.py +54 -0
- nous/genai/cli.py +1316 -0
- nous/genai/client.py +719 -0
- nous/genai/mcp_cli.py +275 -0
- nous/genai/mcp_server.py +1080 -0
- nous/genai/providers/__init__.py +15 -0
- nous/genai/providers/aliyun.py +535 -0
- nous/genai/providers/anthropic.py +483 -0
- nous/genai/providers/gemini.py +1606 -0
- nous/genai/providers/openai.py +1909 -0
- nous/genai/providers/tuzi.py +1158 -0
- nous/genai/providers/volcengine.py +273 -0
- nous/genai/reference/__init__.py +17 -0
- nous/genai/reference/catalog.py +206 -0
- nous/genai/reference/mappings.py +467 -0
- nous/genai/reference/mode_overrides.py +26 -0
- nous/genai/reference/model_catalog.py +82 -0
- nous/genai/reference/model_catalog_data/__init__.py +1 -0
- nous/genai/reference/model_catalog_data/aliyun.py +98 -0
- nous/genai/reference/model_catalog_data/anthropic.py +10 -0
- nous/genai/reference/model_catalog_data/google.py +45 -0
- nous/genai/reference/model_catalog_data/openai.py +44 -0
- nous/genai/reference/model_catalog_data/tuzi_anthropic.py +21 -0
- nous/genai/reference/model_catalog_data/tuzi_google.py +19 -0
- nous/genai/reference/model_catalog_data/tuzi_openai.py +75 -0
- nous/genai/reference/model_catalog_data/tuzi_web.py +136 -0
- nous/genai/reference/model_catalog_data/volcengine.py +107 -0
- nous/genai/tools/__init__.py +13 -0
- nous/genai/tools/output_parser.py +119 -0
- nous/genai/types.py +416 -0
- nous/py.typed +1 -0
- nous_genai-0.1.0.dist-info/METADATA +200 -0
- nous_genai-0.1.0.dist-info/RECORD +45 -0
- nous_genai-0.1.0.dist-info/WHEEL +5 -0
- nous_genai-0.1.0.dist-info/entry_points.txt +4 -0
- nous_genai-0.1.0.dist-info/licenses/LICENSE +190 -0
- nous_genai-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any, Iterator
|
|
6
|
+
from uuid import uuid4
|
|
7
|
+
|
|
8
|
+
from .._internal.errors import (
|
|
9
|
+
invalid_request_error,
|
|
10
|
+
not_supported_error,
|
|
11
|
+
provider_error,
|
|
12
|
+
)
|
|
13
|
+
from .._internal.http import request_json
|
|
14
|
+
from ..types import Capability, GenerateEvent, GenerateRequest, GenerateResponse
|
|
15
|
+
from ..types import JobInfo, Message, Part, PartSourceUrl
|
|
16
|
+
from .openai import OpenAIAdapter
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True, slots=True)
|
|
20
|
+
class VolcengineAdapter:
|
|
21
|
+
"""
|
|
22
|
+
Volcengine Ark (Doubao).
|
|
23
|
+
|
|
24
|
+
Supported in this SDK:
|
|
25
|
+
- chat (text/image -> text), stream supported
|
|
26
|
+
- embeddings (text -> embedding) for text embedding models
|
|
27
|
+
- image generation (text -> image) for Seedream models
|
|
28
|
+
- video generation (text -> video) for Seedance models via content generation tasks
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
openai: OpenAIAdapter
|
|
32
|
+
|
|
33
|
+
def capabilities(self, model_id: str) -> Capability:
|
|
34
|
+
if _is_text_embedding_model(model_id):
|
|
35
|
+
return Capability(
|
|
36
|
+
input_modalities={"text"},
|
|
37
|
+
output_modalities={"embedding"},
|
|
38
|
+
supports_stream=False,
|
|
39
|
+
supports_job=False,
|
|
40
|
+
supports_tools=False,
|
|
41
|
+
supports_json_schema=False,
|
|
42
|
+
)
|
|
43
|
+
if _is_seedream_model(model_id):
|
|
44
|
+
return Capability(
|
|
45
|
+
input_modalities={"text"},
|
|
46
|
+
output_modalities={"image"},
|
|
47
|
+
supports_stream=False,
|
|
48
|
+
supports_job=False,
|
|
49
|
+
supports_tools=False,
|
|
50
|
+
supports_json_schema=False,
|
|
51
|
+
)
|
|
52
|
+
if _is_seedance_video_model(model_id):
|
|
53
|
+
return Capability(
|
|
54
|
+
input_modalities={"text"},
|
|
55
|
+
output_modalities={"video"},
|
|
56
|
+
supports_stream=False,
|
|
57
|
+
supports_job=True,
|
|
58
|
+
supports_tools=False,
|
|
59
|
+
supports_json_schema=False,
|
|
60
|
+
)
|
|
61
|
+
return Capability(
|
|
62
|
+
input_modalities={"text", "image"},
|
|
63
|
+
output_modalities={"text"},
|
|
64
|
+
supports_stream=True,
|
|
65
|
+
supports_job=False,
|
|
66
|
+
supports_tools=True,
|
|
67
|
+
supports_json_schema=True,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def list_models(self, *, timeout_ms: int | None = None) -> list[str]:
|
|
71
|
+
return self.openai.list_models(timeout_ms=timeout_ms)
|
|
72
|
+
|
|
73
|
+
def generate(
|
|
74
|
+
self, request: GenerateRequest, *, stream: bool
|
|
75
|
+
) -> GenerateResponse | Iterator[GenerateEvent]:
|
|
76
|
+
modalities = set(request.output.modalities)
|
|
77
|
+
model_id = request.model_id()
|
|
78
|
+
|
|
79
|
+
if modalities == {"embedding"}:
|
|
80
|
+
if stream:
|
|
81
|
+
raise not_supported_error(
|
|
82
|
+
"Volcengine embeddings do not support streaming"
|
|
83
|
+
)
|
|
84
|
+
if not _is_text_embedding_model(model_id):
|
|
85
|
+
raise not_supported_error(
|
|
86
|
+
"Volcengine embedding requires a text embedding model"
|
|
87
|
+
)
|
|
88
|
+
return self.openai.generate(request, stream=False)
|
|
89
|
+
|
|
90
|
+
if modalities == {"image"}:
|
|
91
|
+
if stream:
|
|
92
|
+
raise not_supported_error(
|
|
93
|
+
"Volcengine image generation does not support streaming"
|
|
94
|
+
)
|
|
95
|
+
if not _is_seedream_model(model_id):
|
|
96
|
+
raise not_supported_error(
|
|
97
|
+
"Volcengine image generation requires a Seedream model"
|
|
98
|
+
)
|
|
99
|
+
return self.openai.generate(request, stream=False)
|
|
100
|
+
|
|
101
|
+
if modalities == {"video"}:
|
|
102
|
+
if stream:
|
|
103
|
+
raise not_supported_error(
|
|
104
|
+
"Volcengine video generation does not support streaming"
|
|
105
|
+
)
|
|
106
|
+
return self._video(request, model_id=model_id)
|
|
107
|
+
|
|
108
|
+
if modalities != {"text"}:
|
|
109
|
+
raise not_supported_error(
|
|
110
|
+
"Volcengine only supports text chat, embeddings, Seedream images, and Seedance video in this SDK"
|
|
111
|
+
)
|
|
112
|
+
if _is_text_embedding_model(model_id):
|
|
113
|
+
raise not_supported_error(
|
|
114
|
+
"Volcengine embedding models must be called with output.modalities=['embedding']"
|
|
115
|
+
)
|
|
116
|
+
if _is_seedream_model(model_id):
|
|
117
|
+
raise not_supported_error(
|
|
118
|
+
"Volcengine Seedream models must be called with output.modalities=['image']"
|
|
119
|
+
)
|
|
120
|
+
if _is_seedance_video_model(model_id):
|
|
121
|
+
raise not_supported_error(
|
|
122
|
+
"Volcengine Seedance models must be called with output.modalities=['video']"
|
|
123
|
+
)
|
|
124
|
+
if _has_audio_input(request):
|
|
125
|
+
raise not_supported_error(
|
|
126
|
+
"Volcengine chat input does not support audio in this SDK"
|
|
127
|
+
)
|
|
128
|
+
return self.openai.generate(request, stream=stream)
|
|
129
|
+
|
|
130
|
+
def _video(self, request: GenerateRequest, *, model_id: str) -> GenerateResponse:
|
|
131
|
+
if not _is_seedance_video_model(model_id):
|
|
132
|
+
raise not_supported_error(
|
|
133
|
+
'Volcengine video generation requires model like "volcengine:doubao-seedance-1-0-lite-t2v-250428"'
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
prompt = _single_text_prompt(request)
|
|
137
|
+
body: dict[str, Any] = {
|
|
138
|
+
"model": model_id,
|
|
139
|
+
"content": [{"type": "text", "text": prompt}],
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
opts = request.provider_options.get("volcengine")
|
|
143
|
+
if isinstance(opts, dict):
|
|
144
|
+
if "model" in opts and opts["model"] != model_id:
|
|
145
|
+
raise invalid_request_error("provider_options cannot override model")
|
|
146
|
+
body.update({k: v for k, v in opts.items() if k != "model"})
|
|
147
|
+
|
|
148
|
+
budget_ms = (
|
|
149
|
+
120_000 if request.params.timeout_ms is None else request.params.timeout_ms
|
|
150
|
+
)
|
|
151
|
+
deadline = time.time() + max(1, budget_ms) / 1000.0
|
|
152
|
+
obj = request_json(
|
|
153
|
+
method="POST",
|
|
154
|
+
url=f"{self.openai.base_url}/contents/generations/tasks",
|
|
155
|
+
headers=_headers(self.openai.api_key, request=request),
|
|
156
|
+
json_body=body,
|
|
157
|
+
timeout_ms=min(30_000, max(1, budget_ms)),
|
|
158
|
+
proxy_url=self.openai.proxy_url,
|
|
159
|
+
)
|
|
160
|
+
task_id = obj.get("id")
|
|
161
|
+
if not isinstance(task_id, str) or not task_id:
|
|
162
|
+
raise provider_error("volcengine video response missing task id")
|
|
163
|
+
|
|
164
|
+
if not request.wait:
|
|
165
|
+
return GenerateResponse(
|
|
166
|
+
id=f"sdk_{uuid4().hex}",
|
|
167
|
+
provider="volcengine",
|
|
168
|
+
model=f"volcengine:{model_id}",
|
|
169
|
+
status="running",
|
|
170
|
+
job=JobInfo(job_id=task_id, poll_after_ms=1_000),
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
final = _wait_task_done(
|
|
174
|
+
base_url=self.openai.base_url,
|
|
175
|
+
api_key=self.openai.api_key,
|
|
176
|
+
task_id=task_id,
|
|
177
|
+
deadline=deadline,
|
|
178
|
+
proxy_url=self.openai.proxy_url,
|
|
179
|
+
)
|
|
180
|
+
status = final.get("status")
|
|
181
|
+
if status != "succeeded":
|
|
182
|
+
if status == "failed":
|
|
183
|
+
raise provider_error(f"volcengine video generation failed: {final}")
|
|
184
|
+
return GenerateResponse(
|
|
185
|
+
id=f"sdk_{uuid4().hex}",
|
|
186
|
+
provider="volcengine",
|
|
187
|
+
model=f"volcengine:{model_id}",
|
|
188
|
+
status="running",
|
|
189
|
+
job=JobInfo(job_id=task_id, poll_after_ms=1_000),
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
content = final.get("content")
|
|
193
|
+
video_url = content.get("video_url") if isinstance(content, dict) else None
|
|
194
|
+
if not isinstance(video_url, str) or not video_url:
|
|
195
|
+
raise provider_error("volcengine video task missing video_url")
|
|
196
|
+
part = Part(
|
|
197
|
+
type="video", mime_type="video/mp4", source=PartSourceUrl(url=video_url)
|
|
198
|
+
)
|
|
199
|
+
return GenerateResponse(
|
|
200
|
+
id=f"sdk_{uuid4().hex}",
|
|
201
|
+
provider="volcengine",
|
|
202
|
+
model=f"volcengine:{model_id}",
|
|
203
|
+
status="completed",
|
|
204
|
+
output=[Message(role="assistant", content=[part])],
|
|
205
|
+
usage=None,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _is_text_embedding_model(model_id: str) -> bool:
|
|
210
|
+
model_id = model_id.lower()
|
|
211
|
+
return "embedding" in model_id and "text" in model_id
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _is_seedream_model(model_id: str) -> bool:
|
|
215
|
+
return "seedream" in model_id.lower()
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _is_seedance_video_model(model_id: str) -> bool:
|
|
219
|
+
mid = model_id.lower()
|
|
220
|
+
return "seedance" in mid and ("t2v" in mid or "i2v" in mid)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _single_text_prompt(request: GenerateRequest) -> str:
|
|
224
|
+
texts: list[str] = []
|
|
225
|
+
for m in request.input:
|
|
226
|
+
for p in m.content:
|
|
227
|
+
if p.type != "text":
|
|
228
|
+
raise invalid_request_error(
|
|
229
|
+
"this operation requires exactly one text part"
|
|
230
|
+
)
|
|
231
|
+
t = p.require_text().strip()
|
|
232
|
+
if t:
|
|
233
|
+
texts.append(t)
|
|
234
|
+
if len(texts) != 1:
|
|
235
|
+
raise invalid_request_error("this operation requires exactly one text part")
|
|
236
|
+
return texts[0]
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _headers(api_key: str, *, request: GenerateRequest | None = None) -> dict[str, str]:
|
|
240
|
+
headers = {"Authorization": f"Bearer {api_key}"}
|
|
241
|
+
if request and request.params.idempotency_key:
|
|
242
|
+
headers["Idempotency-Key"] = request.params.idempotency_key
|
|
243
|
+
return headers
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _wait_task_done(
|
|
247
|
+
*, base_url: str, api_key: str, task_id: str, deadline: float, proxy_url: str | None
|
|
248
|
+
) -> dict[str, Any]:
|
|
249
|
+
url = f"{base_url}/contents/generations/tasks/{task_id}"
|
|
250
|
+
while True:
|
|
251
|
+
remaining_ms = int((deadline - time.time()) * 1000)
|
|
252
|
+
if remaining_ms <= 0:
|
|
253
|
+
break
|
|
254
|
+
obj = request_json(
|
|
255
|
+
method="GET",
|
|
256
|
+
url=url,
|
|
257
|
+
headers=_headers(api_key),
|
|
258
|
+
timeout_ms=min(30_000, remaining_ms),
|
|
259
|
+
proxy_url=proxy_url,
|
|
260
|
+
)
|
|
261
|
+
st = obj.get("status")
|
|
262
|
+
if st in {"succeeded", "failed", "cancelled"}:
|
|
263
|
+
return obj
|
|
264
|
+
time.sleep(1.0)
|
|
265
|
+
return {"id": task_id, "status": "running"}
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _has_audio_input(request: GenerateRequest) -> bool:
|
|
269
|
+
for m in request.input:
|
|
270
|
+
for p in m.content:
|
|
271
|
+
if p.type == "audio":
|
|
272
|
+
return True
|
|
273
|
+
return False
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .catalog import (
|
|
4
|
+
get_model_catalog,
|
|
5
|
+
get_sdk_supported_models,
|
|
6
|
+
get_sdk_supported_models_for_provider,
|
|
7
|
+
get_supported_providers,
|
|
8
|
+
)
|
|
9
|
+
from .mappings import get_parameter_mappings
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"get_model_catalog",
|
|
13
|
+
"get_parameter_mappings",
|
|
14
|
+
"get_sdk_supported_models",
|
|
15
|
+
"get_sdk_supported_models_for_provider",
|
|
16
|
+
"get_supported_providers",
|
|
17
|
+
]
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
from typing import Any, Literal
|
|
5
|
+
|
|
6
|
+
from ..types import Capability
|
|
7
|
+
|
|
8
|
+
from .model_catalog import MODEL_CATALOG
|
|
9
|
+
from .mode_overrides import CAPABILITY_OVERRIDES
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_model_catalog() -> dict[str, list[str]]:
|
|
13
|
+
return copy.deepcopy(MODEL_CATALOG)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_supported_providers() -> list[str]:
|
|
17
|
+
return list(MODEL_CATALOG.keys())
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_sdk_supported_models() -> list[dict[str, Any]]:
|
|
21
|
+
"""
|
|
22
|
+
Return a JSON-friendly table of all curated models and their SDK-level capabilities.
|
|
23
|
+
"""
|
|
24
|
+
out: list[dict[str, Any]] = []
|
|
25
|
+
for provider, model_ids in MODEL_CATALOG.items():
|
|
26
|
+
protocol = _default_protocol(provider)
|
|
27
|
+
for model_id in model_ids:
|
|
28
|
+
cap = _capability_for(
|
|
29
|
+
provider=provider, protocol=protocol, model_id=model_id
|
|
30
|
+
)
|
|
31
|
+
cap = _apply_capability_overrides(
|
|
32
|
+
provider=provider, model_id=model_id, cap=cap
|
|
33
|
+
)
|
|
34
|
+
category = _category_for_capability(cap)
|
|
35
|
+
modes = _modes_for(cap)
|
|
36
|
+
notes: list[str] = []
|
|
37
|
+
if cap.supports_job:
|
|
38
|
+
notes.append("可能返回 running(job),需轮询/等待")
|
|
39
|
+
out.append(
|
|
40
|
+
{
|
|
41
|
+
"provider": provider,
|
|
42
|
+
"model_id": model_id,
|
|
43
|
+
"model": f"{provider}:{model_id}",
|
|
44
|
+
"category": category,
|
|
45
|
+
"protocol": protocol,
|
|
46
|
+
"modes": modes,
|
|
47
|
+
"input_modalities": sorted(cap.input_modalities),
|
|
48
|
+
"output_modalities": sorted(cap.output_modalities),
|
|
49
|
+
"supports_job": cap.supports_job,
|
|
50
|
+
"notes": notes,
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
return out
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def get_sdk_supported_models_for_provider(provider: str) -> list[dict[str, Any]]:
|
|
57
|
+
"""
|
|
58
|
+
Return SDK-curated supported models for a single provider (same rows as `get_sdk_supported_models()`).
|
|
59
|
+
|
|
60
|
+
Note: `provider` must match keys used in `MODEL_CATALOG` (e.g. "google", not "gemini").
|
|
61
|
+
"""
|
|
62
|
+
p = provider.strip().lower()
|
|
63
|
+
if not p:
|
|
64
|
+
return []
|
|
65
|
+
return [m for m in get_sdk_supported_models() if m.get("provider") == p]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _default_protocol(provider: str) -> str:
|
|
69
|
+
if provider in {"google", "tuzi-google"}:
|
|
70
|
+
return "gemini"
|
|
71
|
+
if provider in {"anthropic", "tuzi-anthropic"}:
|
|
72
|
+
return "messages"
|
|
73
|
+
if provider == "tuzi-web":
|
|
74
|
+
return "multi"
|
|
75
|
+
return "chat_completions"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _category_for_capability(cap: Capability) -> str:
|
|
79
|
+
out = set(cap.output_modalities)
|
|
80
|
+
if out == {"embedding"}:
|
|
81
|
+
return "embedding"
|
|
82
|
+
if out == {"image"}:
|
|
83
|
+
return "image"
|
|
84
|
+
if out == {"video"}:
|
|
85
|
+
return "video"
|
|
86
|
+
if out == {"audio"}:
|
|
87
|
+
return "audio"
|
|
88
|
+
if (
|
|
89
|
+
out == {"text"}
|
|
90
|
+
and "audio" in set(cap.input_modalities)
|
|
91
|
+
and not cap.supports_stream
|
|
92
|
+
):
|
|
93
|
+
return "transcription"
|
|
94
|
+
return "chat"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _capability_for(*, provider: str, protocol: str, model_id: str) -> Capability:
|
|
98
|
+
if provider in {"openai", "tuzi-openai"}:
|
|
99
|
+
from ..providers import OpenAIAdapter
|
|
100
|
+
|
|
101
|
+
return OpenAIAdapter(
|
|
102
|
+
api_key="__demo__", provider_name=provider, chat_api=protocol
|
|
103
|
+
).capabilities(model_id)
|
|
104
|
+
|
|
105
|
+
if provider in {"google", "tuzi-google"}:
|
|
106
|
+
from ..providers import GeminiAdapter
|
|
107
|
+
|
|
108
|
+
gemini_auth_mode: Literal["bearer", "query_key"] = (
|
|
109
|
+
"bearer" if provider.startswith("tuzi-") else "query_key"
|
|
110
|
+
)
|
|
111
|
+
return GeminiAdapter(
|
|
112
|
+
api_key="__demo__", provider_name=provider, auth_mode=gemini_auth_mode
|
|
113
|
+
).capabilities(model_id)
|
|
114
|
+
|
|
115
|
+
if provider in {"anthropic", "tuzi-anthropic"}:
|
|
116
|
+
from ..providers import AnthropicAdapter
|
|
117
|
+
|
|
118
|
+
anthropic_auth_mode: Literal["bearer", "x-api-key"] = (
|
|
119
|
+
"bearer" if provider.startswith("tuzi-") else "x-api-key"
|
|
120
|
+
)
|
|
121
|
+
return AnthropicAdapter(
|
|
122
|
+
api_key="__demo__", provider_name=provider, auth_mode=anthropic_auth_mode
|
|
123
|
+
).capabilities(model_id)
|
|
124
|
+
|
|
125
|
+
if provider == "volcengine":
|
|
126
|
+
from ..providers import OpenAIAdapter, VolcengineAdapter
|
|
127
|
+
|
|
128
|
+
volc = VolcengineAdapter(
|
|
129
|
+
openai=OpenAIAdapter(
|
|
130
|
+
api_key="__demo__",
|
|
131
|
+
provider_name="volcengine",
|
|
132
|
+
chat_api="chat_completions",
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
return volc.capabilities(model_id)
|
|
136
|
+
|
|
137
|
+
if provider == "aliyun":
|
|
138
|
+
from ..providers import AliyunAdapter, OpenAIAdapter
|
|
139
|
+
|
|
140
|
+
aliyun = AliyunAdapter(
|
|
141
|
+
openai=OpenAIAdapter(
|
|
142
|
+
api_key="__demo__", provider_name="aliyun", chat_api="chat_completions"
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
return aliyun.capabilities(model_id)
|
|
146
|
+
|
|
147
|
+
if provider == "tuzi-web":
|
|
148
|
+
from ..providers import (
|
|
149
|
+
AnthropicAdapter,
|
|
150
|
+
GeminiAdapter,
|
|
151
|
+
OpenAIAdapter,
|
|
152
|
+
TuziAdapter,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
return TuziAdapter(
|
|
156
|
+
openai=OpenAIAdapter(
|
|
157
|
+
api_key="__demo__", provider_name=provider, chat_api="chat_completions"
|
|
158
|
+
),
|
|
159
|
+
gemini=GeminiAdapter(
|
|
160
|
+
api_key="__demo__",
|
|
161
|
+
base_url="https://api.tu-zi.com",
|
|
162
|
+
provider_name=provider,
|
|
163
|
+
auth_mode="bearer",
|
|
164
|
+
supports_file_upload=False,
|
|
165
|
+
),
|
|
166
|
+
anthropic=AnthropicAdapter(
|
|
167
|
+
api_key="__demo__", provider_name=provider, auth_mode="bearer"
|
|
168
|
+
),
|
|
169
|
+
).capabilities(model_id)
|
|
170
|
+
|
|
171
|
+
raise ValueError(f"unknown provider: {provider}")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _apply_capability_overrides(
|
|
175
|
+
*, provider: str, model_id: str, cap: Capability
|
|
176
|
+
) -> Capability:
|
|
177
|
+
overrides = CAPABILITY_OVERRIDES.get(provider)
|
|
178
|
+
if not overrides:
|
|
179
|
+
return cap
|
|
180
|
+
override = overrides.get(model_id)
|
|
181
|
+
if not override:
|
|
182
|
+
return cap
|
|
183
|
+
supports_stream = cap.supports_stream
|
|
184
|
+
if "supports_stream" in override:
|
|
185
|
+
supports_stream = override["supports_stream"]
|
|
186
|
+
supports_job = cap.supports_job
|
|
187
|
+
if "supports_job" in override:
|
|
188
|
+
supports_job = override["supports_job"]
|
|
189
|
+
if supports_stream == cap.supports_stream and supports_job == cap.supports_job:
|
|
190
|
+
return cap
|
|
191
|
+
return Capability(
|
|
192
|
+
input_modalities=set(cap.input_modalities),
|
|
193
|
+
output_modalities=set(cap.output_modalities),
|
|
194
|
+
supports_stream=supports_stream,
|
|
195
|
+
supports_job=supports_job,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _modes_for(cap: Capability) -> list[str]:
|
|
200
|
+
modes: list[str] = ["sync"]
|
|
201
|
+
if cap.supports_stream:
|
|
202
|
+
modes.append("stream")
|
|
203
|
+
if cap.supports_job:
|
|
204
|
+
modes.append("job")
|
|
205
|
+
modes.append("async")
|
|
206
|
+
return modes
|