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.
Files changed (45) hide show
  1. nous/__init__.py +3 -0
  2. nous/genai/__init__.py +56 -0
  3. nous/genai/__main__.py +3 -0
  4. nous/genai/_internal/__init__.py +1 -0
  5. nous/genai/_internal/capability_rules.py +476 -0
  6. nous/genai/_internal/config.py +102 -0
  7. nous/genai/_internal/errors.py +63 -0
  8. nous/genai/_internal/http.py +951 -0
  9. nous/genai/_internal/json_schema.py +54 -0
  10. nous/genai/cli.py +1316 -0
  11. nous/genai/client.py +719 -0
  12. nous/genai/mcp_cli.py +275 -0
  13. nous/genai/mcp_server.py +1080 -0
  14. nous/genai/providers/__init__.py +15 -0
  15. nous/genai/providers/aliyun.py +535 -0
  16. nous/genai/providers/anthropic.py +483 -0
  17. nous/genai/providers/gemini.py +1606 -0
  18. nous/genai/providers/openai.py +1909 -0
  19. nous/genai/providers/tuzi.py +1158 -0
  20. nous/genai/providers/volcengine.py +273 -0
  21. nous/genai/reference/__init__.py +17 -0
  22. nous/genai/reference/catalog.py +206 -0
  23. nous/genai/reference/mappings.py +467 -0
  24. nous/genai/reference/mode_overrides.py +26 -0
  25. nous/genai/reference/model_catalog.py +82 -0
  26. nous/genai/reference/model_catalog_data/__init__.py +1 -0
  27. nous/genai/reference/model_catalog_data/aliyun.py +98 -0
  28. nous/genai/reference/model_catalog_data/anthropic.py +10 -0
  29. nous/genai/reference/model_catalog_data/google.py +45 -0
  30. nous/genai/reference/model_catalog_data/openai.py +44 -0
  31. nous/genai/reference/model_catalog_data/tuzi_anthropic.py +21 -0
  32. nous/genai/reference/model_catalog_data/tuzi_google.py +19 -0
  33. nous/genai/reference/model_catalog_data/tuzi_openai.py +75 -0
  34. nous/genai/reference/model_catalog_data/tuzi_web.py +136 -0
  35. nous/genai/reference/model_catalog_data/volcengine.py +107 -0
  36. nous/genai/tools/__init__.py +13 -0
  37. nous/genai/tools/output_parser.py +119 -0
  38. nous/genai/types.py +416 -0
  39. nous/py.typed +1 -0
  40. nous_genai-0.1.0.dist-info/METADATA +200 -0
  41. nous_genai-0.1.0.dist-info/RECORD +45 -0
  42. nous_genai-0.1.0.dist-info/WHEEL +5 -0
  43. nous_genai-0.1.0.dist-info/entry_points.txt +4 -0
  44. nous_genai-0.1.0.dist-info/licenses/LICENSE +190 -0
  45. nous_genai-0.1.0.dist-info/top_level.txt +1 -0
nous/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """
2
+ Nous namespace package.
3
+ """
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,3 @@
1
+ from .cli import main
2
+
3
+ main()
@@ -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
+ )