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
nous/__init__.py
ADDED
nous/genai/__init__.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from . import reference
|
|
2
|
+
from ._internal.errors import ErrorInfo, GenAIError
|
|
3
|
+
from .client import Client
|
|
4
|
+
from .types import (
|
|
5
|
+
Capability,
|
|
6
|
+
GenerateEvent,
|
|
7
|
+
GenerateParams,
|
|
8
|
+
GenerateRequest,
|
|
9
|
+
GenerateResponse,
|
|
10
|
+
JobInfo,
|
|
11
|
+
Message,
|
|
12
|
+
OutputAudioSpec,
|
|
13
|
+
OutputEmbeddingSpec,
|
|
14
|
+
OutputImageSpec,
|
|
15
|
+
OutputSpec,
|
|
16
|
+
OutputTextSpec,
|
|
17
|
+
OutputVideoSpec,
|
|
18
|
+
Part,
|
|
19
|
+
PartSourceBytes,
|
|
20
|
+
PartSourcePath,
|
|
21
|
+
PartSourceRef,
|
|
22
|
+
PartSourceUrl,
|
|
23
|
+
ReasoningSpec,
|
|
24
|
+
Tool,
|
|
25
|
+
ToolChoice,
|
|
26
|
+
Usage,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"Capability",
|
|
31
|
+
"Client",
|
|
32
|
+
"ErrorInfo",
|
|
33
|
+
"GenerateEvent",
|
|
34
|
+
"GenerateParams",
|
|
35
|
+
"GenerateRequest",
|
|
36
|
+
"GenerateResponse",
|
|
37
|
+
"GenAIError",
|
|
38
|
+
"JobInfo",
|
|
39
|
+
"Message",
|
|
40
|
+
"OutputAudioSpec",
|
|
41
|
+
"OutputEmbeddingSpec",
|
|
42
|
+
"OutputImageSpec",
|
|
43
|
+
"OutputSpec",
|
|
44
|
+
"OutputTextSpec",
|
|
45
|
+
"OutputVideoSpec",
|
|
46
|
+
"Part",
|
|
47
|
+
"PartSourceBytes",
|
|
48
|
+
"PartSourcePath",
|
|
49
|
+
"PartSourceRef",
|
|
50
|
+
"PartSourceUrl",
|
|
51
|
+
"ReasoningSpec",
|
|
52
|
+
"Tool",
|
|
53
|
+
"ToolChoice",
|
|
54
|
+
"Usage",
|
|
55
|
+
"reference",
|
|
56
|
+
]
|
nous/genai/__main__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Central place for capability rules based on model series (usually shared prefixes).
|
|
3
|
+
|
|
4
|
+
Rules here only look at model_id strings and are provider-agnostic. Provider adapters
|
|
5
|
+
still own protocol details (stream/job semantics, routing, endpoint quirks).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from typing import Final, Literal
|
|
12
|
+
|
|
13
|
+
from ..types import Modality
|
|
14
|
+
|
|
15
|
+
ModelKind = Literal["video", "image", "embedding", "tts", "transcribe", "chat"]
|
|
16
|
+
GeminiModelKind = Literal["video", "embedding", "tts", "native_audio", "image", "chat"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _mods(*values: Modality) -> set[Modality]:
|
|
20
|
+
return set(values)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _norm(model_id: str) -> str:
|
|
24
|
+
return model_id.lower().strip()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _starts_with_any(s: str, prefixes: tuple[str, ...]) -> bool:
|
|
28
|
+
return s.startswith(prefixes)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
_TOKEN_SPLIT_RE: Final[re.Pattern[str]] = re.compile(r"[^a-z0-9]+")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _has_token(s: str, token: str) -> bool:
|
|
35
|
+
# Token boundary: split by any non [a-z0-9] to avoid substring false-positives.
|
|
36
|
+
toks = _TOKEN_SPLIT_RE.split(s.lower())
|
|
37
|
+
return token.lower() in toks
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ---- Model series for /v1-style protocols ----
|
|
41
|
+
|
|
42
|
+
_SORA_PREFIX: Final[str] = "sora-"
|
|
43
|
+
|
|
44
|
+
_VEO_PREFIX: Final[str] = "veo"
|
|
45
|
+
_PIKA_PREFIX: Final[str] = "pika-"
|
|
46
|
+
_RUNWAY_PREFIX: Final[str] = "runway-"
|
|
47
|
+
_SEEDANCE_PREFIXES: Final[tuple[str, ...]] = ("seedance", "doubao-seedance")
|
|
48
|
+
_KLING_VIDEO_PREFIXES: Final[tuple[str, ...]] = ("kling_video", "kling-video-")
|
|
49
|
+
|
|
50
|
+
_DALLE_PREFIX: Final[str] = "dall-e-"
|
|
51
|
+
_GPT_IMAGE_PREFIX: Final[str] = "gpt-image-"
|
|
52
|
+
_CHATGPT_IMAGE_PREFIX: Final[str] = "chatgpt-image"
|
|
53
|
+
_SEEDREAM_PREFIXES: Final[tuple[str, ...]] = ("seedream", "doubao-seedream")
|
|
54
|
+
_SEEDEDIT_PREFIXES: Final[tuple[str, ...]] = ("seededit", "api-images-seededit")
|
|
55
|
+
_FLUX_PREFIXES: Final[tuple[str, ...]] = ("flux-", "flux.")
|
|
56
|
+
_SD3_PREFIX: Final[str] = "sd3"
|
|
57
|
+
|
|
58
|
+
_TEXT_EMBEDDING_PREFIX: Final[str] = "text-embedding-"
|
|
59
|
+
_EMBEDDING_PREFIX: Final[str] = "embedding-"
|
|
60
|
+
_DOUBAO_EMBEDDING_PREFIX: Final[str] = "doubao-embedding-"
|
|
61
|
+
|
|
62
|
+
_TTS_PREFIX: Final[str] = "tts-"
|
|
63
|
+
_TTS_SUFFIX: Final[str] = "-tts"
|
|
64
|
+
_VOICE_SUFFIXES: Final[tuple[str, ...]] = ("-voice", "_voice")
|
|
65
|
+
_ADVANCED_VOICE_MODEL: Final[str] = "advanced-voice"
|
|
66
|
+
_SUNO_PREFIX: Final[str] = "suno-"
|
|
67
|
+
_CHIRP_PREFIX: Final[str] = "chirp-"
|
|
68
|
+
|
|
69
|
+
_WHISPER_PREFIX: Final[str] = "whisper-"
|
|
70
|
+
_DISTIL_WHISPER_PREFIX: Final[str] = "distil-whisper-"
|
|
71
|
+
_TRANSCRIBE_MARKER: Final[str] = "-transcribe"
|
|
72
|
+
_ASR_PREFIX: Final[str] = "asr"
|
|
73
|
+
|
|
74
|
+
_IMAGE_MODELS_WITH_IMAGE_INPUT_PREFIXES: Final[tuple[str, ...]] = (
|
|
75
|
+
"gpt-image-1",
|
|
76
|
+
"chatgpt-image",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
_Z_IMAGE_PREFIX: Final[str] = "z-image"
|
|
80
|
+
|
|
81
|
+
_CHAT_IMAGE_INPUT_EXACT: Final[tuple[str, ...]] = (
|
|
82
|
+
"chatgpt-4o-latest",
|
|
83
|
+
"codex-mini-latest",
|
|
84
|
+
"computer-use-preview",
|
|
85
|
+
"omni-moderation-latest",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
_DEEPSEEK_V3_PREFIX: Final[str] = "deepseek-v3"
|
|
89
|
+
_KIMI_K2_PREFIX: Final[str] = "kimi-k2"
|
|
90
|
+
_KIMI_LATEST_PREFIX: Final[str] = "kimi-latest"
|
|
91
|
+
_QWEN_PREFIX: Final[str] = "qwen"
|
|
92
|
+
_QVQ_PREFIX: Final[str] = "qvq"
|
|
93
|
+
|
|
94
|
+
_CHAT_NO_IMAGE_PREFIXES: Final[tuple[str, ...]] = (
|
|
95
|
+
_DEEPSEEK_V3_PREFIX,
|
|
96
|
+
_KIMI_K2_PREFIX,
|
|
97
|
+
_QVQ_PREFIX,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
_CHAT_TEXT_ONLY_EXACT: Final[tuple[str, ...]] = (
|
|
101
|
+
"gpt-4",
|
|
102
|
+
"gpt-4-turbo-preview",
|
|
103
|
+
"o1-mini",
|
|
104
|
+
"o1-preview",
|
|
105
|
+
"o3-mini",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
_CHAT_TEXT_ONLY_MARKERS: Final[tuple[str, ...]] = ("search-preview",)
|
|
109
|
+
|
|
110
|
+
_CHAT_AUDIO_PREFIXES: Final[tuple[str, ...]] = ("gpt-audio", "gpt-realtime")
|
|
111
|
+
_CHAT_AUDIO_SUFFIXES: Final[tuple[str, ...]] = ("-audio-preview", "-realtime-preview")
|
|
112
|
+
|
|
113
|
+
_CLAUDE_PREFIX: Final[str] = "claude-"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def is_sora_model(model_id: str) -> bool:
|
|
117
|
+
return _norm(model_id).startswith(_SORA_PREFIX)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def is_veo_model(model_id: str) -> bool:
|
|
121
|
+
return _norm(model_id).startswith(_VEO_PREFIX)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def is_pika_model(model_id: str) -> bool:
|
|
125
|
+
return _norm(model_id).startswith(_PIKA_PREFIX)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def is_runway_model(model_id: str) -> bool:
|
|
129
|
+
return _norm(model_id).startswith(_RUNWAY_PREFIX)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def is_seedance_model(model_id: str) -> bool:
|
|
133
|
+
return _starts_with_any(_norm(model_id), _SEEDANCE_PREFIXES)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def is_kling_video_model(model_id: str) -> bool:
|
|
137
|
+
return _starts_with_any(_norm(model_id), _KLING_VIDEO_PREFIXES)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def is_video_model(model_id: str) -> bool:
|
|
141
|
+
return (
|
|
142
|
+
is_sora_model(model_id)
|
|
143
|
+
or is_veo_model(model_id)
|
|
144
|
+
or is_pika_model(model_id)
|
|
145
|
+
or is_runway_model(model_id)
|
|
146
|
+
or is_seedance_model(model_id)
|
|
147
|
+
or is_kling_video_model(model_id)
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def is_dall_e_model(model_id: str) -> bool:
|
|
152
|
+
return _norm(model_id).startswith(_DALLE_PREFIX)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def is_gpt_image_model(model_id: str) -> bool:
|
|
156
|
+
return _norm(model_id).startswith(_GPT_IMAGE_PREFIX)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def is_chatgpt_image_model(model_id: str) -> bool:
|
|
160
|
+
return _norm(model_id).startswith(_CHATGPT_IMAGE_PREFIX)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def is_seedream_model(model_id: str) -> bool:
|
|
164
|
+
return _starts_with_any(_norm(model_id), _SEEDREAM_PREFIXES)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def is_seededit_model(model_id: str) -> bool:
|
|
168
|
+
return _starts_with_any(_norm(model_id), _SEEDEDIT_PREFIXES)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def is_flux_model(model_id: str) -> bool:
|
|
172
|
+
return _starts_with_any(_norm(model_id), _FLUX_PREFIXES)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def is_sd3_model(model_id: str) -> bool:
|
|
176
|
+
return _norm(model_id).startswith(_SD3_PREFIX)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def is_z_image_model(model_id: str) -> bool:
|
|
180
|
+
return _norm(model_id).startswith(_Z_IMAGE_PREFIX)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def is_gpt_dash_image_model(model_id: str) -> bool:
|
|
184
|
+
mid_l = _norm(model_id)
|
|
185
|
+
return mid_l.startswith("gpt-") and "-image" in mid_l
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def is_image_model(model_id: str) -> bool:
|
|
189
|
+
return (
|
|
190
|
+
is_dall_e_model(model_id)
|
|
191
|
+
or is_gpt_image_model(model_id)
|
|
192
|
+
or is_gpt_dash_image_model(model_id)
|
|
193
|
+
or is_chatgpt_image_model(model_id)
|
|
194
|
+
or is_z_image_model(model_id)
|
|
195
|
+
or is_seedream_model(model_id)
|
|
196
|
+
or is_seededit_model(model_id)
|
|
197
|
+
or is_flux_model(model_id)
|
|
198
|
+
or is_sd3_model(model_id)
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def is_text_embedding_model(model_id: str) -> bool:
|
|
203
|
+
return _norm(model_id).startswith(_TEXT_EMBEDDING_PREFIX)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def is_embedding_series_model(model_id: str) -> bool:
|
|
207
|
+
return _norm(model_id).startswith(_EMBEDDING_PREFIX)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def is_doubao_embedding_model(model_id: str) -> bool:
|
|
211
|
+
return _norm(model_id).startswith(_DOUBAO_EMBEDDING_PREFIX)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def is_embedding_model(model_id: str) -> bool:
|
|
215
|
+
return (
|
|
216
|
+
is_text_embedding_model(model_id)
|
|
217
|
+
or is_embedding_series_model(model_id)
|
|
218
|
+
or is_doubao_embedding_model(model_id)
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def is_tts_model(model_id: str) -> bool:
|
|
223
|
+
mid_l = _norm(model_id)
|
|
224
|
+
return (
|
|
225
|
+
mid_l.startswith(_TTS_PREFIX)
|
|
226
|
+
or mid_l.startswith(_SUNO_PREFIX)
|
|
227
|
+
or mid_l.startswith(_CHIRP_PREFIX)
|
|
228
|
+
or mid_l.endswith(_TTS_SUFFIX)
|
|
229
|
+
or mid_l.endswith(_VOICE_SUFFIXES)
|
|
230
|
+
or mid_l == _ADVANCED_VOICE_MODEL
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def is_whisper_model(model_id: str) -> bool:
|
|
235
|
+
return _norm(model_id).startswith(_WHISPER_PREFIX)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def is_distil_whisper_model(model_id: str) -> bool:
|
|
239
|
+
return _norm(model_id).startswith(_DISTIL_WHISPER_PREFIX)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def is_transcribe_marker_model(model_id: str) -> bool:
|
|
243
|
+
return _TRANSCRIBE_MARKER in _norm(model_id)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def is_asr_model(model_id: str) -> bool:
|
|
247
|
+
return _norm(model_id).startswith(_ASR_PREFIX)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def is_transcribe_model(model_id: str) -> bool:
|
|
251
|
+
return (
|
|
252
|
+
is_whisper_model(model_id)
|
|
253
|
+
or is_distil_whisper_model(model_id)
|
|
254
|
+
or is_transcribe_marker_model(model_id)
|
|
255
|
+
or is_asr_model(model_id)
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def infer_model_kind(model_id: str) -> ModelKind:
|
|
260
|
+
if is_video_model(model_id):
|
|
261
|
+
return "video"
|
|
262
|
+
if is_image_model(model_id):
|
|
263
|
+
return "image"
|
|
264
|
+
if is_embedding_model(model_id):
|
|
265
|
+
return "embedding"
|
|
266
|
+
if is_tts_model(model_id):
|
|
267
|
+
return "tts"
|
|
268
|
+
if is_transcribe_model(model_id):
|
|
269
|
+
return "transcribe"
|
|
270
|
+
return "chat"
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def output_modalities_for_kind(kind: ModelKind) -> set[Modality] | None:
|
|
274
|
+
if kind == "video":
|
|
275
|
+
return _mods("video")
|
|
276
|
+
if kind == "image":
|
|
277
|
+
return _mods("image")
|
|
278
|
+
if kind == "embedding":
|
|
279
|
+
return _mods("embedding")
|
|
280
|
+
if kind == "tts":
|
|
281
|
+
return _mods("audio")
|
|
282
|
+
if kind == "transcribe":
|
|
283
|
+
return _mods("text")
|
|
284
|
+
return None
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def transcribe_input_modalities(_: str) -> set[Modality]:
|
|
288
|
+
return _mods("audio")
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def is_claude_model(model_id: str) -> bool:
|
|
292
|
+
return _norm(model_id).startswith(_CLAUDE_PREFIX)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def claude_input_modalities(model_id: str) -> set[Modality]:
|
|
296
|
+
if is_claude_model(model_id):
|
|
297
|
+
return _mods("text", "image")
|
|
298
|
+
return _mods("text")
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def image_input_modalities(model_id: str) -> set[Modality]:
|
|
302
|
+
mid_l = _norm(model_id)
|
|
303
|
+
if _starts_with_any(
|
|
304
|
+
mid_l, _IMAGE_MODELS_WITH_IMAGE_INPUT_PREFIXES
|
|
305
|
+
) or mid_l.startswith(_Z_IMAGE_PREFIX):
|
|
306
|
+
return _mods("text", "image")
|
|
307
|
+
return _mods("text")
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def video_input_modalities(model_id: str) -> set[Modality]:
|
|
311
|
+
if is_veo_model(model_id):
|
|
312
|
+
return _mods("text", "image")
|
|
313
|
+
return _mods("text")
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def chat_input_modalities(model_id: str) -> set[Modality]:
|
|
317
|
+
mid_l = _norm(model_id)
|
|
318
|
+
out: set[Modality] = {"text"}
|
|
319
|
+
if _chat_supports_image_input(mid_l):
|
|
320
|
+
out.add("image")
|
|
321
|
+
if _chat_supports_audio_input(mid_l):
|
|
322
|
+
out.add("audio")
|
|
323
|
+
return out
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def chat_output_modalities(model_id: str) -> set[Modality]:
|
|
327
|
+
mid_l = _norm(model_id)
|
|
328
|
+
out: set[Modality] = {"text"}
|
|
329
|
+
if _chat_supports_audio_io(mid_l):
|
|
330
|
+
out.add("audio")
|
|
331
|
+
return out
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def chat_supports_audio_io(model_id: str) -> bool:
|
|
335
|
+
return _chat_supports_audio_io(_norm(model_id))
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _chat_supports_audio_io(mid_l: str) -> bool:
|
|
339
|
+
return _starts_with_any(mid_l, _CHAT_AUDIO_PREFIXES) or mid_l.endswith(
|
|
340
|
+
_CHAT_AUDIO_SUFFIXES
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _qwen_supports_image_input(mid_l: str) -> bool:
|
|
345
|
+
return _has_token(mid_l, "image") or _has_token(mid_l, "vl")
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _qwen_supports_audio_input(mid_l: str) -> bool:
|
|
349
|
+
return _has_token(mid_l, "asr")
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def chat_supports_audio_input(model_id: str) -> bool:
|
|
353
|
+
return _chat_supports_audio_input(_norm(model_id))
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _chat_supports_audio_input(mid_l: str) -> bool:
|
|
357
|
+
if _chat_supports_audio_io(mid_l):
|
|
358
|
+
return True
|
|
359
|
+
return mid_l.startswith(_QWEN_PREFIX) and _qwen_supports_audio_input(mid_l)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def chat_supports_image_input(model_id: str) -> bool:
|
|
363
|
+
return _chat_supports_image_input(_norm(model_id))
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _chat_supports_image_input(mid_l: str) -> bool:
|
|
367
|
+
if mid_l in _CHAT_TEXT_ONLY_EXACT:
|
|
368
|
+
return False
|
|
369
|
+
if any(marker in mid_l for marker in _CHAT_TEXT_ONLY_MARKERS):
|
|
370
|
+
return False
|
|
371
|
+
|
|
372
|
+
if _starts_with_any(mid_l, _CHAT_NO_IMAGE_PREFIXES):
|
|
373
|
+
return False
|
|
374
|
+
if mid_l.startswith(_KIMI_LATEST_PREFIX):
|
|
375
|
+
return True
|
|
376
|
+
if mid_l.startswith(_QWEN_PREFIX):
|
|
377
|
+
return _qwen_supports_image_input(mid_l)
|
|
378
|
+
|
|
379
|
+
if mid_l in _CHAT_IMAGE_INPUT_EXACT:
|
|
380
|
+
return True
|
|
381
|
+
|
|
382
|
+
return _openai_style_chat_supports_image_input(mid_l)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _openai_style_chat_supports_image_input(mid_l: str) -> bool:
|
|
386
|
+
if mid_l.startswith("gpt-realtime"):
|
|
387
|
+
return True
|
|
388
|
+
if mid_l.startswith("gpt-4o") and not _chat_supports_audio_io(mid_l):
|
|
389
|
+
return True
|
|
390
|
+
if mid_l.startswith("gpt-4-turbo") and mid_l != "gpt-4-turbo-preview":
|
|
391
|
+
return True
|
|
392
|
+
if mid_l.startswith(("gpt-4.1", "gpt-4.5", "gpt-5")):
|
|
393
|
+
return True
|
|
394
|
+
if mid_l == "o1" or mid_l.startswith("o1-"):
|
|
395
|
+
return True
|
|
396
|
+
if mid_l == "o3" or mid_l.startswith("o3-"):
|
|
397
|
+
return True
|
|
398
|
+
return mid_l.startswith("o4-")
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
# ---- Gemini model series ----
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _gemini_norm(model_id: str) -> str:
|
|
405
|
+
mid = model_id.lower().strip()
|
|
406
|
+
if mid.startswith("models/"):
|
|
407
|
+
mid = mid[len("models/") :]
|
|
408
|
+
return mid
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _gemini_is_image_model(mid_l: str) -> bool:
|
|
412
|
+
return (
|
|
413
|
+
mid_l.startswith("imagen-")
|
|
414
|
+
or "image-generation" in mid_l
|
|
415
|
+
or mid_l.endswith("-image")
|
|
416
|
+
or mid_l.endswith("-image-preview")
|
|
417
|
+
or "-image-" in mid_l
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def gemini_is_video_model(model_id: str) -> bool:
|
|
422
|
+
return _gemini_norm(model_id).startswith("veo")
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def gemini_is_embedding_model(model_id: str) -> bool:
|
|
426
|
+
return "embedding" in _gemini_norm(model_id)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def gemini_is_tts_model(model_id: str) -> bool:
|
|
430
|
+
return "tts" in _gemini_norm(model_id)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def gemini_is_native_audio_model(model_id: str) -> bool:
|
|
434
|
+
return "native-audio" in _gemini_norm(model_id)
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def gemini_is_image_model(model_id: str) -> bool:
|
|
438
|
+
return _gemini_is_image_model(_gemini_norm(model_id))
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def gemini_image_input_modalities(model_id: str) -> set[Modality]:
|
|
442
|
+
mid_l = _gemini_norm(model_id)
|
|
443
|
+
if mid_l.startswith("imagen-"):
|
|
444
|
+
return _mods("text")
|
|
445
|
+
if "image-generation" in mid_l:
|
|
446
|
+
return _mods("text")
|
|
447
|
+
return _mods("text", "image")
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def gemini_model_kind(model_id: str) -> GeminiModelKind:
|
|
451
|
+
mid_l = _gemini_norm(model_id)
|
|
452
|
+
if mid_l.startswith("veo"):
|
|
453
|
+
return "video"
|
|
454
|
+
if "embedding" in mid_l:
|
|
455
|
+
return "embedding"
|
|
456
|
+
if "tts" in mid_l:
|
|
457
|
+
return "tts"
|
|
458
|
+
if "native-audio" in mid_l:
|
|
459
|
+
return "native_audio"
|
|
460
|
+
if _gemini_is_image_model(mid_l):
|
|
461
|
+
return "image"
|
|
462
|
+
return "chat"
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def gemini_output_modalities(kind: GeminiModelKind) -> set[Modality]:
|
|
466
|
+
if kind == "video":
|
|
467
|
+
return _mods("video")
|
|
468
|
+
if kind == "embedding":
|
|
469
|
+
return _mods("embedding")
|
|
470
|
+
if kind == "tts":
|
|
471
|
+
return _mods("audio")
|
|
472
|
+
if kind == "native_audio":
|
|
473
|
+
return _mods("text", "audio")
|
|
474
|
+
if kind == "image":
|
|
475
|
+
return _mods("image")
|
|
476
|
+
return _mods("text")
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
_ENV_PRIORITY = (".env.local", ".env.production", ".env.development", ".env.test")
|
|
9
|
+
|
|
10
|
+
_ENV_PREFIX = "NOUS_GENAI_"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_prefixed_env(name: str) -> str | None:
|
|
14
|
+
return os.environ.get(f"{_ENV_PREFIX}{name}")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _parse_env_line(line: str) -> tuple[str, str] | None:
|
|
18
|
+
stripped = line.strip()
|
|
19
|
+
if not stripped or stripped.startswith("#"):
|
|
20
|
+
return None
|
|
21
|
+
if "=" not in stripped:
|
|
22
|
+
return None
|
|
23
|
+
key, value = stripped.split("=", 1)
|
|
24
|
+
key = key.strip()
|
|
25
|
+
value = value.strip()
|
|
26
|
+
if not key:
|
|
27
|
+
return None
|
|
28
|
+
if (value.startswith("'") and value.endswith("'")) or (
|
|
29
|
+
value.startswith('"') and value.endswith('"')
|
|
30
|
+
):
|
|
31
|
+
value = value[1:-1]
|
|
32
|
+
return key, value
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def load_env_files(root: str | Path | None = None) -> list[Path]:
|
|
36
|
+
"""
|
|
37
|
+
Load env files by priority:
|
|
38
|
+
`.env.local > .env.production > .env.development > .env.test`.
|
|
39
|
+
|
|
40
|
+
Implementation: apply higher priority first without overriding existing env.
|
|
41
|
+
"""
|
|
42
|
+
base = Path(root) if root is not None else Path.cwd()
|
|
43
|
+
loaded: list[Path] = []
|
|
44
|
+
for name in _ENV_PRIORITY:
|
|
45
|
+
path = base / name
|
|
46
|
+
if not path.is_file():
|
|
47
|
+
continue
|
|
48
|
+
loaded.append(path)
|
|
49
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
50
|
+
parsed = _parse_env_line(line)
|
|
51
|
+
if parsed is None:
|
|
52
|
+
continue
|
|
53
|
+
key, value = parsed
|
|
54
|
+
os.environ.setdefault(key, value)
|
|
55
|
+
return loaded
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True, slots=True)
|
|
59
|
+
class ProviderKeys:
|
|
60
|
+
openai_api_key: str | None
|
|
61
|
+
google_api_key: str | None
|
|
62
|
+
anthropic_api_key: str | None
|
|
63
|
+
aliyun_api_key: str | None
|
|
64
|
+
volcengine_api_key: str | None
|
|
65
|
+
tuzi_web_api_key: str | None
|
|
66
|
+
tuzi_openai_api_key: str | None
|
|
67
|
+
tuzi_google_api_key: str | None
|
|
68
|
+
tuzi_anthropic_api_key: str | None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def get_provider_keys() -> ProviderKeys:
|
|
72
|
+
return ProviderKeys(
|
|
73
|
+
openai_api_key=get_prefixed_env("OPENAI_API_KEY")
|
|
74
|
+
or os.environ.get("OPENAI_API_KEY"),
|
|
75
|
+
google_api_key=get_prefixed_env("GOOGLE_API_KEY")
|
|
76
|
+
or os.environ.get("GOOGLE_API_KEY"),
|
|
77
|
+
anthropic_api_key=get_prefixed_env("ANTHROPIC_API_KEY")
|
|
78
|
+
or os.environ.get("ANTHROPIC_API_KEY"),
|
|
79
|
+
aliyun_api_key=get_prefixed_env("ALIYUN_API_KEY")
|
|
80
|
+
or os.environ.get("ALIYUN_API_KEY"),
|
|
81
|
+
volcengine_api_key=get_prefixed_env("VOLCENGINE_API_KEY")
|
|
82
|
+
or os.environ.get("VOLCENGINE_API_KEY"),
|
|
83
|
+
tuzi_web_api_key=get_prefixed_env("TUZI_WEB_API_KEY")
|
|
84
|
+
or os.environ.get("TUZI_WEB_API_KEY"),
|
|
85
|
+
tuzi_openai_api_key=get_prefixed_env("TUZI_OPENAI_API_KEY")
|
|
86
|
+
or os.environ.get("TUZI_OPENAI_API_KEY"),
|
|
87
|
+
tuzi_google_api_key=get_prefixed_env("TUZI_GOOGLE_API_KEY")
|
|
88
|
+
or os.environ.get("TUZI_GOOGLE_API_KEY"),
|
|
89
|
+
tuzi_anthropic_api_key=get_prefixed_env("TUZI_ANTHROPIC_API_KEY")
|
|
90
|
+
or os.environ.get("TUZI_ANTHROPIC_API_KEY"),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def get_default_timeout_ms() -> int:
|
|
95
|
+
raw = get_prefixed_env("TIMEOUT_MS")
|
|
96
|
+
if raw is None:
|
|
97
|
+
return 120_000
|
|
98
|
+
try:
|
|
99
|
+
value = int(raw)
|
|
100
|
+
except ValueError:
|
|
101
|
+
return 120_000
|
|
102
|
+
return max(1, value)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True, slots=True)
|
|
7
|
+
class ErrorInfo:
|
|
8
|
+
type: str
|
|
9
|
+
message: str
|
|
10
|
+
provider_code: str | None = None
|
|
11
|
+
retryable: bool = False
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class GenAIError(RuntimeError):
|
|
15
|
+
def __init__(self, info: ErrorInfo):
|
|
16
|
+
super().__init__(info.message)
|
|
17
|
+
self.info = info
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def auth_error(message: str, provider_code: str | None = None) -> GenAIError:
|
|
21
|
+
return GenAIError(
|
|
22
|
+
ErrorInfo(type="AuthError", message=message, provider_code=provider_code)
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def rate_limit_error(message: str, provider_code: str | None = None) -> GenAIError:
|
|
27
|
+
return GenAIError(
|
|
28
|
+
ErrorInfo(
|
|
29
|
+
type="RateLimitError",
|
|
30
|
+
message=message,
|
|
31
|
+
provider_code=provider_code,
|
|
32
|
+
retryable=True,
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def invalid_request_error(message: str, provider_code: str | None = None) -> GenAIError:
|
|
38
|
+
return GenAIError(
|
|
39
|
+
ErrorInfo(
|
|
40
|
+
type="InvalidRequestError", message=message, provider_code=provider_code
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def not_supported_error(message: str) -> GenAIError:
|
|
46
|
+
return GenAIError(ErrorInfo(type="NotSupportedError", message=message))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def timeout_error(message: str) -> GenAIError:
|
|
50
|
+
return GenAIError(ErrorInfo(type="TimeoutError", message=message, retryable=True))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def provider_error(
|
|
54
|
+
message: str, provider_code: str | None = None, retryable: bool = False
|
|
55
|
+
) -> GenAIError:
|
|
56
|
+
return GenAIError(
|
|
57
|
+
ErrorInfo(
|
|
58
|
+
type="ProviderError",
|
|
59
|
+
message=message,
|
|
60
|
+
provider_code=provider_code,
|
|
61
|
+
retryable=retryable,
|
|
62
|
+
)
|
|
63
|
+
)
|