licos-dev-cli 0.4.9__tar.gz → 0.6.0__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.
@@ -0,0 +1,25 @@
1
+ Metadata-Version: 2.4
2
+ Name: licos-dev-cli
3
+ Version: 0.6.0
4
+ Summary: LICOS Dev CLI - generate files and call model capabilities
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: click>=8.1
7
+ Requires-Dist: licos-dev-sdk>=0.6.0
8
+ Description-Content-Type: text/markdown
9
+
10
+ # licos-dev CLI
11
+
12
+ 模型目录、模型调用和 `credit-check` 支持:
13
+
14
+ - `--identity user`(默认):用 `LICOS_CURRENT_USER_ID` 指定的容器执行用户换取 Token。
15
+ - `--identity project`:使用项目身份。
16
+ - `--token-stdin`:从标准输入读取本次请求的用户 Token,不含 `Bearer ` 前缀。不能与 `--identity project` 同时使用。
17
+
18
+ `LICOS_CURRENT_USER_ID` 不是网页登录者。HTTP 服务调用 CLI 时,必须为用户操作使用 `--token-stdin`。缺少用户 ID、空 Token 或鉴权失败均报错,不切换到其他身份。
19
+
20
+ ```bash
21
+ licos-dev credit-check --identity project
22
+ licos-dev model-catalog --identity user
23
+ ```
24
+
25
+ 文档、图片排版等本地文件工具不需要用户身份选项。完整约定见仓库的 `tools/IDENTITY.md`。
@@ -0,0 +1,16 @@
1
+ # licos-dev CLI
2
+
3
+ 模型目录、模型调用和 `credit-check` 支持:
4
+
5
+ - `--identity user`(默认):用 `LICOS_CURRENT_USER_ID` 指定的容器执行用户换取 Token。
6
+ - `--identity project`:使用项目身份。
7
+ - `--token-stdin`:从标准输入读取本次请求的用户 Token,不含 `Bearer ` 前缀。不能与 `--identity project` 同时使用。
8
+
9
+ `LICOS_CURRENT_USER_ID` 不是网页登录者。HTTP 服务调用 CLI 时,必须为用户操作使用 `--token-stdin`。缺少用户 ID、空 Token 或鉴权失败均报错,不切换到其他身份。
10
+
11
+ ```bash
12
+ licos-dev credit-check --identity project
13
+ licos-dev model-catalog --identity user
14
+ ```
15
+
16
+ 文档、图片排版等本地文件工具不需要用户身份选项。完整约定见仓库的 `tools/IDENTITY.md`。
@@ -4,11 +4,12 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "licos-dev-cli"
7
- version = "0.4.9"
7
+ version = "0.6.0"
8
8
  description = "LICOS Dev CLI - generate files and call model capabilities"
9
+ readme = "README.md"
9
10
  requires-python = ">=3.10"
10
11
  dependencies = [
11
- "licos-dev-sdk>=0.4.9",
12
+ "licos-dev-sdk>=0.6.0",
12
13
  "click>=8.1",
13
14
  ]
14
15
 
@@ -0,0 +1,36 @@
1
+ """Explicit identity selection for model and credit CLI commands."""
2
+
3
+ import os
4
+ import sys
5
+ import click
6
+ from licos_platform_sdk import PlatformSdkError, runtime_user_token
7
+
8
+
9
+ def identity_options(command):
10
+ command = click.option("--token-stdin", is_flag=True, help="Read the request user Token from stdin")(command)
11
+ return click.option(
12
+ "--identity",
13
+ type=click.Choice(["user", "project"]),
14
+ default="user",
15
+ show_default=True,
16
+ help="User uses LICOS_CURRENT_USER_ID; project uses project runtime credentials",
17
+ )(command)
18
+
19
+
20
+ def user_token(identity: str, token_stdin: bool) -> str | None:
21
+ if token_stdin:
22
+ if identity == "project":
23
+ raise click.UsageError("--token-stdin cannot be combined with --identity project")
24
+ token = sys.stdin.read().strip()
25
+ if not token:
26
+ raise click.UsageError("--token-stdin requires a nonempty user Token")
27
+ return token
28
+ if identity == "project":
29
+ return None
30
+ user_id = os.environ.get("LICOS_CURRENT_USER_ID", "").strip()
31
+ if not user_id:
32
+ raise click.UsageError("User identity requires LICOS_CURRENT_USER_ID or --token-stdin")
33
+ try:
34
+ return runtime_user_token(user_id)
35
+ except PlatformSdkError as error:
36
+ raise click.ClickException(str(error)) from error
@@ -7,6 +7,7 @@ from pathlib import Path
7
7
  import click
8
8
 
9
9
  from .ppt_master import ppt_master
10
+ from .identity import identity_options, user_token
10
11
  from .docx_commands import DOCX_COMMANDS
11
12
  from .common import _read_input, _read_json, _json_option, _echo_json, _path_to_cli_output, _echo_path
12
13
 
@@ -30,7 +31,8 @@ for command in DOCX_COMMANDS:
30
31
  @click.option("--resource-type", default="MODEL_TOOL", show_default=True)
31
32
  @click.option("--resource-code", default="MANUAL_CHECK", show_default=True)
32
33
  @click.option("--request-id", default=None)
33
- def credit_check(estimated_points, resource_type, resource_code, request_id):
34
+ @identity_options
35
+ def credit_check(estimated_points, resource_type, resource_code, request_id, identity, token_stdin):
34
36
  """Check whether the current SDK identity can execute a billed operation."""
35
37
  from licos_dev_sdk import check_credit_availability
36
38
 
@@ -40,6 +42,7 @@ def credit_check(estimated_points, resource_type, resource_code, request_id):
40
42
  resource_type=resource_type,
41
43
  resource_code=resource_code,
42
44
  request_id=request_id,
45
+ user_token=user_token(identity, token_stdin),
43
46
  )
44
47
  )
45
48
 
@@ -547,16 +550,20 @@ def html_cmd(input_path, content, filename, output_dir, content_type):
547
550
 
548
551
  # ── Model Catalog ────────────────────────────────────────────────────────────
549
552
 
553
+
550
554
  @cli.command("model-catalog")
551
555
  @click.option("--refresh", is_flag=True, help="Bypass local catalog cache")
552
556
  @click.option("--workspace-id", default=None, help="Workspace id; defaults to runtime workspace")
553
- @click.option("--input-capabilities", default=None, help="Optional input capabilities filter, for example text or image")
557
+ @click.option(
558
+ "--input-capabilities", default=None, help="Optional input capabilities filter, for example text or image"
559
+ )
554
560
  @click.option(
555
561
  "--category-code",
556
562
  default="llm",
557
563
  help="Model category code, for example llm, image_generation, video_generation",
558
564
  )
559
- def model_catalog(refresh, workspace_id, input_capabilities, category_code):
565
+ @identity_options
566
+ def model_catalog(refresh, workspace_id, input_capabilities, category_code, identity, token_stdin):
560
567
  """Print platform model capability catalog."""
561
568
  from licos_dev_sdk import fetch_model_catalogs
562
569
 
@@ -566,6 +573,7 @@ def model_catalog(refresh, workspace_id, input_capabilities, category_code):
566
573
  workspace_id=workspace_id,
567
574
  category_code=category_code,
568
575
  input_capabilities=input_capabilities,
576
+ user_token=user_token(identity, token_stdin),
569
577
  )
570
578
  )
571
579
 
@@ -585,7 +593,8 @@ def llm_group():
585
593
  @click.option("--temperature", default=None, type=float)
586
594
  @click.option("--max-completion-tokens", default=None, type=int)
587
595
  @click.option("--extra", default=None, help="Extra request fields as JSON object")
588
- def llm_invoke(prompt, messages, model, temperature, max_completion_tokens, extra):
596
+ @identity_options
597
+ def llm_invoke(prompt, messages, model, temperature, max_completion_tokens, extra, identity, token_stdin):
589
598
  """Run a non-streaming chat completion."""
590
599
  from licos_dev_sdk import LLMClient
591
600
 
@@ -596,7 +605,7 @@ def llm_invoke(prompt, messages, model, temperature, max_completion_tokens, extr
596
605
  extra_payload = _json_option(extra, "--extra") or {}
597
606
  if not isinstance(extra_payload, dict):
598
607
  raise click.BadParameter("--extra must be a JSON object")
599
- result = LLMClient().invoke(
608
+ result = LLMClient(user_token=user_token(identity, token_stdin)).invoke(
600
609
  message_payload,
601
610
  model=model,
602
611
  temperature=temperature,
@@ -613,7 +622,8 @@ def llm_invoke(prompt, messages, model, temperature, max_completion_tokens, extr
613
622
  @click.option("--temperature", default=None, type=float)
614
623
  @click.option("--max-completion-tokens", default=None, type=int)
615
624
  @click.option("--extra", default=None, help="Extra request fields as JSON object")
616
- def llm_stream(prompt, messages, model, temperature, max_completion_tokens, extra):
625
+ @identity_options
626
+ def llm_stream(prompt, messages, model, temperature, max_completion_tokens, extra, identity, token_stdin):
617
627
  """Run a streaming chat completion and print text chunks."""
618
628
  from licos_dev_sdk import LLMClient
619
629
 
@@ -624,7 +634,7 @@ def llm_stream(prompt, messages, model, temperature, max_completion_tokens, extr
624
634
  extra_payload = _json_option(extra, "--extra") or {}
625
635
  if not isinstance(extra_payload, dict):
626
636
  raise click.BadParameter("--extra must be a JSON object")
627
- for chunk in LLMClient().stream(
637
+ for chunk in LLMClient(user_token=user_token(identity, token_stdin)).stream(
628
638
  message_payload,
629
639
  model=model,
630
640
  temperature=temperature,
@@ -643,14 +653,17 @@ def vision_group():
643
653
 
644
654
 
645
655
  @vision_group.command("understand")
646
- @click.option("--image", "image", multiple=True, help="Image URL, Base64 data URI, or plain Base64 image data. Can be repeated.")
656
+ @click.option(
657
+ "--image", "image", multiple=True, help="Image URL, Base64 data URI, or plain Base64 image data. Can be repeated."
658
+ )
647
659
  @click.option("--image-url", "image_url", multiple=True, help="Alias for --image. Can be repeated.")
648
660
  @click.option("--file-path", "file_path", multiple=True, help="Local image file path. Can be repeated.")
649
661
  @click.option("-p", "--prompt", default="Describe this image.")
650
662
  @click.option("--model", default=None)
651
663
  @click.option("--mime-type", default=None, help="MIME type for plain Base64 image inputs. Defaults to image/png.")
652
664
  @click.option("--raw-request", default=None, help="Raw JSON request body")
653
- def vision_understand(image, image_url, file_path, prompt, model, mime_type, raw_request):
665
+ @identity_options
666
+ def vision_understand(image, image_url, file_path, prompt, model, mime_type, raw_request, identity, token_stdin):
654
667
  """Understand one or more images with the catalog vision model."""
655
668
  from licos_dev_sdk import VisionClient
656
669
 
@@ -659,7 +672,7 @@ def vision_understand(image, image_url, file_path, prompt, model, mime_type, raw
659
672
  raise click.BadParameter("--raw-request must be a JSON object")
660
673
  if raw_payload is None and not image_url and not image and not file_path:
661
674
  raise click.UsageError("Provide --image, --image-url, --file-path, or --raw-request.")
662
- result = VisionClient().understand(
675
+ result = VisionClient(user_token=user_token(identity, token_stdin)).understand(
663
676
  images=[*image, *image_url],
664
677
  file_paths=list(file_path),
665
678
  prompt=prompt,
@@ -692,6 +705,7 @@ def image_generation_group():
692
705
  @click.option("--parameters", default=None, help="Parameters JSON object")
693
706
  @click.option("--raw-request", default=None, help="Raw JSON request body")
694
707
  @click.option("--no-wait", is_flag=True, help="Return submit response without polling async tasks")
708
+ @identity_options
695
709
  def image_generate(
696
710
  prompt,
697
711
  image,
@@ -706,6 +720,8 @@ def image_generate(
706
720
  parameters,
707
721
  raw_request,
708
722
  no_wait,
723
+ identity,
724
+ token_stdin,
709
725
  ):
710
726
  """Generate images from text, image URLs/Base64, or local image files."""
711
727
  from licos_dev_sdk import ImageGenerationClient
@@ -718,7 +734,7 @@ def image_generate(
718
734
  raise click.BadParameter("--raw-request must be a JSON object")
719
735
  if raw_payload is None and not prompt:
720
736
  raise click.UsageError("Provide --prompt or --raw-request.")
721
- result = ImageGenerationClient().generate(
737
+ result = ImageGenerationClient(user_token=user_token(identity, token_stdin)).generate(
722
738
  prompt or "",
723
739
  negative_prompt=negative_prompt,
724
740
  count=count,
@@ -756,6 +772,7 @@ def video_generation_group():
756
772
  @click.option("--parameters", default=None, help="Parameters JSON object")
757
773
  @click.option("--raw-request", default=None, help="Raw JSON request body")
758
774
  @click.option("--no-wait", is_flag=True, help="Return submit response without polling async tasks")
775
+ @identity_options
759
776
  def video_generate(
760
777
  prompt,
761
778
  image,
@@ -769,6 +786,8 @@ def video_generate(
769
786
  parameters,
770
787
  raw_request,
771
788
  no_wait,
789
+ identity,
790
+ token_stdin,
772
791
  ):
773
792
  """Generate a video from a text prompt and optional first-frame image."""
774
793
  from licos_dev_sdk import VideoGenerationClient
@@ -781,7 +800,7 @@ def video_generate(
781
800
  raise click.BadParameter("--raw-request must be a JSON object")
782
801
  if raw_payload is None and not prompt:
783
802
  raise click.UsageError("Provide --prompt or --raw-request.")
784
- result = VideoGenerationClient().generate(
803
+ result = VideoGenerationClient(user_token=user_token(identity, token_stdin)).generate(
785
804
  prompt or "",
786
805
  image=image,
787
806
  image_url=image_url,
@@ -807,7 +826,9 @@ def audio_group():
807
826
 
808
827
 
809
828
  @audio_group.command("recognize")
810
- @click.option("--audio", "audio", multiple=True, help="Audio URL, Base64 data URI, or plain Base64 audio data. Can be repeated.")
829
+ @click.option(
830
+ "--audio", "audio", multiple=True, help="Audio URL, Base64 data URI, or plain Base64 audio data. Can be repeated."
831
+ )
811
832
  @click.option("--audio-url", "audio_url", multiple=True, help="Alias for --audio. Can be repeated.")
812
833
  @click.option("--file-path", "file_path", multiple=True, help="Local audio file path. Can be repeated.")
813
834
  @click.option("--model", default=None)
@@ -815,7 +836,10 @@ def audio_group():
815
836
  @click.option("--parameters", default=None, help="Parameters JSON object")
816
837
  @click.option("--raw-request", default=None, help="Raw JSON request body")
817
838
  @click.option("--no-wait", is_flag=True, help="Return submit response without polling async tasks")
818
- def audio_recognize(audio, audio_url, file_path, model, mime_type, parameters, raw_request, no_wait):
839
+ @identity_options
840
+ def audio_recognize(
841
+ audio, audio_url, file_path, model, mime_type, parameters, raw_request, no_wait, identity, token_stdin
842
+ ):
819
843
  """Recognize speech from one or more audio inputs."""
820
844
  from licos_dev_sdk import SpeechRecognitionClient
821
845
 
@@ -827,7 +851,7 @@ def audio_recognize(audio, audio_url, file_path, model, mime_type, parameters, r
827
851
  raise click.BadParameter("--raw-request must be a JSON object")
828
852
  if raw_payload is None and not audio and not audio_url and not file_path:
829
853
  raise click.UsageError("Provide --audio, --audio-url, --file-path, or --raw-request.")
830
- result = SpeechRecognitionClient().recognize(
854
+ result = SpeechRecognitionClient(user_token=user_token(identity, token_stdin)).recognize(
831
855
  audios=[*audio, *audio_url],
832
856
  file_paths=list(file_path),
833
857
  model=model,
@@ -19,6 +19,8 @@ def test_credit_check_forwards_structured_options() -> None:
19
19
  cli,
20
20
  [
21
21
  "credit-check",
22
+ "--identity",
23
+ "project",
22
24
  "--estimated-points",
23
25
  "2",
24
26
  "--resource-type",
@@ -33,6 +35,7 @@ def test_credit_check_forwards_structured_options() -> None:
33
35
  assert result.exit_code == 0
34
36
  assert json.loads(result.output) == availability
35
37
  check.assert_called_once_with(
38
+ user_token=None,
36
39
  estimated_points=2,
37
40
  resource_type="MODEL_TOOL",
38
41
  resource_code="IMAGE_GENERATE",
@@ -0,0 +1,44 @@
1
+ import json
2
+ from unittest import mock
3
+
4
+ from click.testing import CliRunner
5
+ from licos_dev_cli.main import cli
6
+
7
+
8
+ def test_model_catalog_defaults_to_cli_execution_user():
9
+ with mock.patch.dict("os.environ", {"LICOS_CURRENT_USER_ID": "initializer"}, clear=True), mock.patch(
10
+ "licos_dev_cli.identity.runtime_user_token", return_value="cli-token"
11
+ ) as exchange, mock.patch("licos_dev_sdk.fetch_model_catalogs", return_value={}) as catalog:
12
+ result = CliRunner().invoke(cli, ["model-catalog"])
13
+ assert result.exit_code == 0, result.output
14
+ exchange.assert_called_once_with("initializer")
15
+ assert catalog.call_args.kwargs["user_token"] == "cli-token"
16
+
17
+
18
+ def test_credit_check_accepts_web_token_from_stdin():
19
+ with mock.patch("licos_dev_cli.identity.runtime_user_token") as exchange, mock.patch(
20
+ "licos_dev_sdk.check_credit_availability", return_value={"allowed": True}
21
+ ) as check:
22
+ result = CliRunner().invoke(cli, ["credit-check", "--token-stdin"], input="web-token\n")
23
+ assert result.exit_code == 0, result.output
24
+ assert json.loads(result.output) == {"allowed": True}
25
+ assert check.call_args.kwargs["user_token"] == "web-token"
26
+ exchange.assert_not_called()
27
+
28
+
29
+ def test_missing_cli_user_does_not_fall_back_to_owner():
30
+ with mock.patch.dict("os.environ", {"LICOS_USER_ID": "owner"}, clear=True), mock.patch(
31
+ "licos_dev_sdk.fetch_model_catalogs"
32
+ ) as catalog:
33
+ result = CliRunner().invoke(cli, ["model-catalog"])
34
+ assert result.exit_code != 0
35
+ assert "LICOS_CURRENT_USER_ID" in result.output
36
+ catalog.assert_not_called()
37
+
38
+
39
+ def test_empty_or_conflicting_identity_options_fail():
40
+ with mock.patch("licos_dev_sdk.check_credit_availability") as check:
41
+ for args in [["--token-stdin"], ["--identity", "project", "--token-stdin"]]:
42
+ result = CliRunner().invoke(cli, ["credit-check", *args], input="")
43
+ assert result.exit_code != 0
44
+ check.assert_not_called()
@@ -1,7 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: licos-dev-cli
3
- Version: 0.4.9
4
- Summary: LICOS Dev CLI - generate files and call model capabilities
5
- Requires-Python: >=3.10
6
- Requires-Dist: click>=8.1
7
- Requires-Dist: licos-dev-sdk>=0.4.9
File without changes