nous-genai 0.1.0__tar.gz → 0.1.2__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 (81) hide show
  1. {nous_genai-0.1.0/nous_genai.egg-info → nous_genai-0.1.2}/PKG-INFO +6 -1
  2. {nous_genai-0.1.0 → nous_genai-0.1.2}/README.md +5 -0
  3. {nous_genai-0.1.0 → nous_genai-0.1.2}/nous/genai/_internal/capability_rules.py +0 -2
  4. {nous_genai-0.1.0 → nous_genai-0.1.2}/nous/genai/cli.py +174 -20
  5. {nous_genai-0.1.0 → nous_genai-0.1.2}/nous/genai/providers/tuzi.py +234 -299
  6. {nous_genai-0.1.0 → nous_genai-0.1.2}/nous/genai/reference/model_catalog_data/tuzi_web.py +2 -2
  7. {nous_genai-0.1.0 → nous_genai-0.1.2}/nous/genai/types.py +2 -0
  8. {nous_genai-0.1.0 → nous_genai-0.1.2/nous_genai.egg-info}/PKG-INFO +6 -1
  9. {nous_genai-0.1.0 → nous_genai-0.1.2}/pyproject.toml +1 -1
  10. nous_genai-0.1.2/tests/test_tuzi_models.py +723 -0
  11. nous_genai-0.1.0/tests/test_tuzi_models.py +0 -332
  12. {nous_genai-0.1.0 → nous_genai-0.1.2}/LICENSE +0 -0
  13. {nous_genai-0.1.0 → nous_genai-0.1.2}/nous/__init__.py +0 -0
  14. {nous_genai-0.1.0 → nous_genai-0.1.2}/nous/genai/__init__.py +0 -0
  15. {nous_genai-0.1.0 → nous_genai-0.1.2}/nous/genai/__main__.py +0 -0
  16. {nous_genai-0.1.0 → nous_genai-0.1.2}/nous/genai/_internal/__init__.py +0 -0
  17. {nous_genai-0.1.0 → nous_genai-0.1.2}/nous/genai/_internal/config.py +0 -0
  18. {nous_genai-0.1.0 → nous_genai-0.1.2}/nous/genai/_internal/errors.py +0 -0
  19. {nous_genai-0.1.0 → nous_genai-0.1.2}/nous/genai/_internal/http.py +0 -0
  20. {nous_genai-0.1.0 → nous_genai-0.1.2}/nous/genai/_internal/json_schema.py +0 -0
  21. {nous_genai-0.1.0 → nous_genai-0.1.2}/nous/genai/client.py +0 -0
  22. {nous_genai-0.1.0 → nous_genai-0.1.2}/nous/genai/mcp_cli.py +0 -0
  23. {nous_genai-0.1.0 → nous_genai-0.1.2}/nous/genai/mcp_server.py +0 -0
  24. {nous_genai-0.1.0 → nous_genai-0.1.2}/nous/genai/providers/__init__.py +0 -0
  25. {nous_genai-0.1.0 → nous_genai-0.1.2}/nous/genai/providers/aliyun.py +0 -0
  26. {nous_genai-0.1.0 → nous_genai-0.1.2}/nous/genai/providers/anthropic.py +0 -0
  27. {nous_genai-0.1.0 → nous_genai-0.1.2}/nous/genai/providers/gemini.py +0 -0
  28. {nous_genai-0.1.0 → nous_genai-0.1.2}/nous/genai/providers/openai.py +0 -0
  29. {nous_genai-0.1.0 → nous_genai-0.1.2}/nous/genai/providers/volcengine.py +0 -0
  30. {nous_genai-0.1.0 → nous_genai-0.1.2}/nous/genai/reference/__init__.py +0 -0
  31. {nous_genai-0.1.0 → nous_genai-0.1.2}/nous/genai/reference/catalog.py +0 -0
  32. {nous_genai-0.1.0 → nous_genai-0.1.2}/nous/genai/reference/mappings.py +0 -0
  33. {nous_genai-0.1.0 → nous_genai-0.1.2}/nous/genai/reference/mode_overrides.py +0 -0
  34. {nous_genai-0.1.0 → nous_genai-0.1.2}/nous/genai/reference/model_catalog.py +0 -0
  35. {nous_genai-0.1.0 → nous_genai-0.1.2}/nous/genai/reference/model_catalog_data/__init__.py +0 -0
  36. {nous_genai-0.1.0 → nous_genai-0.1.2}/nous/genai/reference/model_catalog_data/aliyun.py +0 -0
  37. {nous_genai-0.1.0 → nous_genai-0.1.2}/nous/genai/reference/model_catalog_data/anthropic.py +0 -0
  38. {nous_genai-0.1.0 → nous_genai-0.1.2}/nous/genai/reference/model_catalog_data/google.py +0 -0
  39. {nous_genai-0.1.0 → nous_genai-0.1.2}/nous/genai/reference/model_catalog_data/openai.py +0 -0
  40. {nous_genai-0.1.0 → nous_genai-0.1.2}/nous/genai/reference/model_catalog_data/tuzi_anthropic.py +0 -0
  41. {nous_genai-0.1.0 → nous_genai-0.1.2}/nous/genai/reference/model_catalog_data/tuzi_google.py +0 -0
  42. {nous_genai-0.1.0 → nous_genai-0.1.2}/nous/genai/reference/model_catalog_data/tuzi_openai.py +0 -0
  43. {nous_genai-0.1.0 → nous_genai-0.1.2}/nous/genai/reference/model_catalog_data/volcengine.py +0 -0
  44. {nous_genai-0.1.0 → nous_genai-0.1.2}/nous/genai/tools/__init__.py +0 -0
  45. {nous_genai-0.1.0 → nous_genai-0.1.2}/nous/genai/tools/output_parser.py +0 -0
  46. {nous_genai-0.1.0 → nous_genai-0.1.2}/nous/py.typed +0 -0
  47. {nous_genai-0.1.0 → nous_genai-0.1.2}/nous_genai.egg-info/SOURCES.txt +0 -0
  48. {nous_genai-0.1.0 → nous_genai-0.1.2}/nous_genai.egg-info/dependency_links.txt +0 -0
  49. {nous_genai-0.1.0 → nous_genai-0.1.2}/nous_genai.egg-info/entry_points.txt +0 -0
  50. {nous_genai-0.1.0 → nous_genai-0.1.2}/nous_genai.egg-info/requires.txt +0 -0
  51. {nous_genai-0.1.0 → nous_genai-0.1.2}/nous_genai.egg-info/top_level.txt +0 -0
  52. {nous_genai-0.1.0 → nous_genai-0.1.2}/setup.cfg +0 -0
  53. {nous_genai-0.1.0 → nous_genai-0.1.2}/tests/test_capability_flags.py +0 -0
  54. {nous_genai-0.1.0 → nous_genai-0.1.2}/tests/test_capability_rules.py +0 -0
  55. {nous_genai-0.1.0 → nous_genai-0.1.2}/tests/test_cli_google_download_auth.py +0 -0
  56. {nous_genai-0.1.0 → nous_genai-0.1.2}/tests/test_cli_probe_parsing.py +0 -0
  57. {nous_genai-0.1.0 → nous_genai-0.1.2}/tests/test_cli_prompt_path.py +0 -0
  58. {nous_genai-0.1.0 → nous_genai-0.1.2}/tests/test_cli_subcommands.py +0 -0
  59. {nous_genai-0.1.0 → nous_genai-0.1.2}/tests/test_cli_timeout_hint.py +0 -0
  60. {nous_genai-0.1.0 → nous_genai-0.1.2}/tests/test_cli_video_wait_default.py +0 -0
  61. {nous_genai-0.1.0 → nous_genai-0.1.2}/tests/test_client_protected_url_artifacts.py +0 -0
  62. {nous_genai-0.1.0 → nous_genai-0.1.2}/tests/test_client_timeout.py +0 -0
  63. {nous_genai-0.1.0 → nous_genai-0.1.2}/tests/test_http_error_mapping.py +0 -0
  64. {nous_genai-0.1.0 → nous_genai-0.1.2}/tests/test_http_security.py +0 -0
  65. {nous_genai-0.1.0 → nous_genai-0.1.2}/tests/test_mcp_artifact_limits.py +0 -0
  66. {nous_genai-0.1.0 → nous_genai-0.1.2}/tests/test_mcp_cli_url_resolution.py +0 -0
  67. {nous_genai-0.1.0 → nous_genai-0.1.2}/tests/test_mcp_input_policy.py +0 -0
  68. {nous_genai-0.1.0 → nous_genai-0.1.2}/tests/test_mcp_model_keyword_filter.py +0 -0
  69. {nous_genai-0.1.0 → nous_genai-0.1.2}/tests/test_mcp_server_transports.py +0 -0
  70. {nous_genai-0.1.0 → nous_genai-0.1.2}/tests/test_mcp_token_rules.py +0 -0
  71. {nous_genai-0.1.0 → nous_genai-0.1.2}/tests/test_model_catalog_validation.py +0 -0
  72. {nous_genai-0.1.0 → nous_genai-0.1.2}/tests/test_model_discovery.py +0 -0
  73. {nous_genai-0.1.0 → nous_genai-0.1.2}/tests/test_modes_inference.py +0 -0
  74. {nous_genai-0.1.0 → nous_genai-0.1.2}/tests/test_openai_responses_streaming.py +0 -0
  75. {nous_genai-0.1.0 → nous_genai-0.1.2}/tests/test_output_json_schema.py +0 -0
  76. {nous_genai-0.1.0 → nous_genai-0.1.2}/tests/test_output_parser_tool.py +0 -0
  77. {nous_genai-0.1.0 → nous_genai-0.1.2}/tests/test_reasoning_mapping.py +0 -0
  78. {nous_genai-0.1.0 → nous_genai-0.1.2}/tests/test_review_report_fixes.py +0 -0
  79. {nous_genai-0.1.0 → nous_genai-0.1.2}/tests/test_sse_parser.py +0 -0
  80. {nous_genai-0.1.0 → nous_genai-0.1.2}/tests/test_tool_calling.py +0 -0
  81. {nous_genai-0.1.0 → nous_genai-0.1.2}/tests/test_tuzi_gemini_markdown_image.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nous-genai
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: Single-endpoint GenAI SDK (multi-provider, multimodal)
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: Homepage, https://github.com/gravtice/nous-genai
@@ -168,6 +168,11 @@ If you need to write to file, see `examples/demo.py` (`_write_binary()`), or reu
168
168
  uv run genai --model openai:gpt-4o-mini --prompt "Hello"
169
169
  uv run genai model available --all
170
170
 
171
+ # Tuzi Chirp music
172
+ uv run genai --model tuzi-web:chirp-v3-5 --prompt "Lo-fi hiphop beat, 30s" --no-wait
173
+ # ...later
174
+ uv run genai --model tuzi-web:chirp-v3-5 --job-id "<job_id>" --output-path demo_suno.mp3 --timeout-ms 600000
175
+
171
176
  # MCP Server
172
177
  uv run genai-mcp-server # Streamable HTTP: /mcp, SSE: /sse
173
178
  uv run genai-mcp-cli tools # Debug CLI
@@ -151,6 +151,11 @@ If you need to write to file, see `examples/demo.py` (`_write_binary()`), or reu
151
151
  uv run genai --model openai:gpt-4o-mini --prompt "Hello"
152
152
  uv run genai model available --all
153
153
 
154
+ # Tuzi Chirp music
155
+ uv run genai --model tuzi-web:chirp-v3-5 --prompt "Lo-fi hiphop beat, 30s" --no-wait
156
+ # ...later
157
+ uv run genai --model tuzi-web:chirp-v3-5 --job-id "<job_id>" --output-path demo_suno.mp3 --timeout-ms 600000
158
+
154
159
  # MCP Server
155
160
  uv run genai-mcp-server # Streamable HTTP: /mcp, SSE: /sse
156
161
  uv run genai-mcp-cli tools # Debug CLI
@@ -63,7 +63,6 @@ _TTS_PREFIX: Final[str] = "tts-"
63
63
  _TTS_SUFFIX: Final[str] = "-tts"
64
64
  _VOICE_SUFFIXES: Final[tuple[str, ...]] = ("-voice", "_voice")
65
65
  _ADVANCED_VOICE_MODEL: Final[str] = "advanced-voice"
66
- _SUNO_PREFIX: Final[str] = "suno-"
67
66
  _CHIRP_PREFIX: Final[str] = "chirp-"
68
67
 
69
68
  _WHISPER_PREFIX: Final[str] = "whisper-"
@@ -223,7 +222,6 @@ def is_tts_model(model_id: str) -> bool:
223
222
  mid_l = _norm(model_id)
224
223
  return (
225
224
  mid_l.startswith(_TTS_PREFIX)
226
- or mid_l.startswith(_SUNO_PREFIX)
227
225
  or mid_l.startswith(_CHIRP_PREFIX)
228
226
  or mid_l.endswith(_TTS_SUFFIX)
229
227
  or mid_l.endswith(_VOICE_SUFFIXES)
@@ -109,6 +109,15 @@ def main(argv: list[str] | None = None) -> None:
109
109
  parser.add_argument("--video-path", help="Input video file path")
110
110
  parser.add_argument("--output-path", help="Write output to file (text/json/binary)")
111
111
  parser.add_argument("--ouput-path", dest="output_path", help=argparse.SUPPRESS)
112
+ parser.add_argument(
113
+ "--job-id",
114
+ help="Resume/poll a provider job id (tuzi-web only for now); ignores --prompt/--*-path",
115
+ )
116
+ parser.add_argument(
117
+ "--no-wait",
118
+ action="store_true",
119
+ help="Do not wait for job completion (returns job_id if supported)",
120
+ )
112
121
  parser.add_argument(
113
122
  "--timeout-ms",
114
123
  type=int,
@@ -184,7 +193,25 @@ def main(argv: list[str] | None = None) -> None:
184
193
  except BrokenPipeError:
185
194
  return
186
195
 
196
+ client = Client()
187
197
  provider, model_id = _split_model(args.model)
198
+ _apply_protocol_override(client, provider=provider, protocol=args.protocol)
199
+
200
+ cap = client.capabilities(args.model)
201
+ output = _infer_output_spec(provider=provider, model_id=model_id, cap=cap)
202
+
203
+ if args.job_id:
204
+ _run_job(
205
+ client,
206
+ provider=provider,
207
+ model_id=model_id,
208
+ job_id=str(args.job_id),
209
+ output=output,
210
+ output_path=args.output_path,
211
+ timeout_ms=timeout_ms,
212
+ )
213
+ return
214
+
188
215
  prompt = args.prompt
189
216
  if prompt is None and args.prompt_path:
190
217
  try:
@@ -192,11 +219,6 @@ def main(argv: list[str] | None = None) -> None:
192
219
  prompt = f.read()
193
220
  except OSError as e:
194
221
  raise SystemExit(f"cannot read --prompt-path: {e}") from None
195
- client = Client()
196
- _apply_protocol_override(client, provider=provider, protocol=args.protocol)
197
-
198
- cap = client.capabilities(args.model)
199
- output = _infer_output_spec(provider=provider, model_id=model_id, cap=cap)
200
222
 
201
223
  parts = _build_input_parts(
202
224
  prompt=prompt,
@@ -212,7 +234,7 @@ def main(argv: list[str] | None = None) -> None:
212
234
  model=args.model,
213
235
  input=[Message(role="user", content=parts)],
214
236
  output=output,
215
- wait=True,
237
+ wait=not bool(getattr(args, "no_wait", False)),
216
238
  )
217
239
  if timeout_ms is not None:
218
240
  req = replace(req, params=replace(req.params, timeout_ms=timeout_ms))
@@ -229,21 +251,37 @@ def main(argv: list[str] | None = None) -> None:
229
251
  if resp.job and resp.job.job_id:
230
252
  print(resp.job.job_id)
231
253
  if resp.status == "running":
232
- effective_timeout_ms = timeout_ms
233
- if effective_timeout_ms is None:
234
- effective_timeout_ms = getattr(
235
- client, "_default_timeout_ms", None
254
+ status_note = ""
255
+ if resp.job.last_status:
256
+ status_note += f" upstream_status={resp.job.last_status}"
257
+ if resp.job.last_detail:
258
+ d = resp.job.last_detail
259
+ if len(d) > 200:
260
+ d = d[:200] + "..."
261
+ status_note += f" fail_reason={d}"
262
+ if not req.wait:
263
+ print(
264
+ "[INFO] 已提交任务(未等待完成);已返回 job_id。"
265
+ f"可用 --job-id {resp.job.job_id} 继续轮询/下载。",
266
+ file=sys.stderr,
267
+ )
268
+ else:
269
+ effective_timeout_ms = timeout_ms
270
+ if effective_timeout_ms is None:
271
+ effective_timeout_ms = getattr(
272
+ client, "_default_timeout_ms", None
273
+ )
274
+ timeout_note = (
275
+ f"{effective_timeout_ms}ms"
276
+ if isinstance(effective_timeout_ms, int)
277
+ else "timeout"
278
+ )
279
+ print(
280
+ f"[INFO] 任务仍在运行(等待 {elapsed_s:.1f}s,可能已超时 {timeout_note});已返回 job_id。"
281
+ f"可用 --job-id {resp.job.job_id} 继续轮询/下载,或增大 --timeout-ms 重试。"
282
+ f"{status_note}",
283
+ file=sys.stderr,
236
284
  )
237
- timeout_note = (
238
- f"{effective_timeout_ms}ms"
239
- if isinstance(effective_timeout_ms, int)
240
- else "timeout"
241
- )
242
- print(
243
- f"[INFO] 任务仍在运行(等待 {elapsed_s:.1f}s,可能已超时 {timeout_note});已返回 job_id。"
244
- "可增大 --timeout-ms 或设置 NOUS_GENAI_TIMEOUT_MS 后重试。",
245
- file=sys.stderr,
246
- )
247
285
  if args.output_path:
248
286
  print(
249
287
  f"[INFO] 未写入输出文件:{args.output_path}",
@@ -275,6 +313,122 @@ _DEFAULT_VIDEO_URL = (
275
313
  )
276
314
 
277
315
 
316
+ def _run_job(
317
+ client: Client,
318
+ *,
319
+ provider: str,
320
+ model_id: str,
321
+ job_id: str,
322
+ output: OutputSpec,
323
+ output_path: str | None,
324
+ timeout_ms: int | None,
325
+ ) -> None:
326
+ provider = provider.strip().lower()
327
+ if provider != "tuzi-web":
328
+ raise SystemExit("--job-id only supported for provider=tuzi-web for now")
329
+
330
+ job_id = job_id.strip()
331
+ if not job_id:
332
+ raise SystemExit("--job-id must be non-empty")
333
+
334
+ adapter = client._adapter(provider)
335
+ from .providers import TuziAdapter
336
+
337
+ if not isinstance(adapter, TuziAdapter):
338
+ raise SystemExit("tuzi-web adapter not configured")
339
+
340
+ effective_timeout_ms = timeout_ms
341
+ if effective_timeout_ms is None:
342
+ effective_timeout_ms = getattr(client, "_default_timeout_ms", None)
343
+ if effective_timeout_ms is None:
344
+ effective_timeout_ms = 120_000
345
+
346
+ modalities = set(output.modalities)
347
+ mid_l = model_id.lower().strip()
348
+ is_chirp_music = (
349
+ modalities == {"audio"} and mid_l.startswith("chirp-") and mid_l != "chirp-v3"
350
+ )
351
+ if not is_chirp_music:
352
+ raise SystemExit("--job-id only supports tuzi-web chirp-* audio tasks for now")
353
+
354
+ def fn():
355
+ try:
356
+ host = adapter._base_host()
357
+ probe = adapter._suno_feed(
358
+ host=host,
359
+ ids=job_id,
360
+ timeout_ms=min(10_000, int(effective_timeout_ms)),
361
+ )
362
+ clips = probe.get("clips")
363
+ clip_found = bool(
364
+ isinstance(clips, list)
365
+ and any(
366
+ isinstance(c, dict)
367
+ and isinstance(c.get("id"), str)
368
+ and c.get("id") == job_id
369
+ for c in clips
370
+ )
371
+ )
372
+ if clip_found:
373
+ return adapter._suno_wait_feed_audio(
374
+ clip_id=job_id,
375
+ model_id=model_id,
376
+ timeout_ms=effective_timeout_ms,
377
+ wait=True,
378
+ )
379
+ except Exception:
380
+ pass
381
+ return adapter._suno_wait_fetch_audio(
382
+ task_id=job_id,
383
+ model_id=model_id,
384
+ timeout_ms=effective_timeout_ms,
385
+ wait=True,
386
+ )
387
+
388
+ show_progress = sys.stderr.isatty()
389
+ resp, elapsed_s = _run_with_spinner(fn, enabled=show_progress, label="等待任务完成")
390
+
391
+ if resp.status != "completed":
392
+ if resp.job and resp.job.job_id:
393
+ print(resp.job.job_id)
394
+ if resp.status == "running":
395
+ status_note = ""
396
+ if resp.job.last_status:
397
+ status_note += f" upstream_status={resp.job.last_status}"
398
+ if resp.job.last_detail:
399
+ d = resp.job.last_detail
400
+ if len(d) > 200:
401
+ d = d[:200] + "..."
402
+ status_note += f" fail_reason={d}"
403
+ timeout_note = (
404
+ f"{effective_timeout_ms}ms"
405
+ if isinstance(effective_timeout_ms, int)
406
+ else "timeout"
407
+ )
408
+ print(
409
+ f"[INFO] 任务仍在运行(等待 {elapsed_s:.1f}s,可能已超时 {timeout_note});已返回 job_id。"
410
+ f"可稍后重试 --job-id。{status_note}",
411
+ file=sys.stderr,
412
+ )
413
+ if output_path:
414
+ print(f"[INFO] 未写入输出文件:{output_path}", file=sys.stderr)
415
+ else:
416
+ raise SystemExit(f"[FAIL]: request status={resp.status}")
417
+ return
418
+
419
+ if not resp.output:
420
+ raise SystemExit("[FAIL]: missing output")
421
+ _write_response(
422
+ resp.output[0].content,
423
+ output=output,
424
+ output_path=output_path,
425
+ timeout_ms=timeout_ms,
426
+ download_auth=_download_auth(client, provider=provider),
427
+ )
428
+ if show_progress:
429
+ print(f"[INFO] 完成,用时 {elapsed_s:.1f}s", file=sys.stderr)
430
+
431
+
278
432
  def _run_probe(args: argparse.Namespace, *, timeout_ms: int | None) -> int:
279
433
  from .client import _normalize_provider
280
434
  from .reference import get_sdk_supported_models_for_provider