crewai-suno 0.1.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.
- crewai_suno-0.1.0/.env.example +1 -0
- crewai_suno-0.1.0/.github/workflows/test.yml +24 -0
- crewai_suno-0.1.0/.gitignore +9 -0
- crewai_suno-0.1.0/LICENSE +21 -0
- crewai_suno-0.1.0/PKG-INFO +133 -0
- crewai_suno-0.1.0/PRIVACY.md +7 -0
- crewai_suno-0.1.0/README.md +115 -0
- crewai_suno-0.1.0/README.pypi.md +115 -0
- crewai_suno-0.1.0/assets/icon.jpg +0 -0
- crewai_suno-0.1.0/assets/tutorial/create-api-key-en.png +0 -0
- crewai_suno-0.1.0/assets/tutorial/get-api-key-en.png +0 -0
- crewai_suno-0.1.0/assets/tutorial/official-crewai-tracing.png +0 -0
- crewai_suno-0.1.0/assets/tutorial/source.json +8 -0
- crewai_suno-0.1.0/examples/query_existing.py +10 -0
- crewai_suno-0.1.0/pyproject.toml +36 -0
- crewai_suno-0.1.0/readme/README_zh_Hans.md +105 -0
- crewai_suno-0.1.0/src/suno_crewai/__init__.py +5 -0
- crewai_suno-0.1.0/src/suno_crewai/_base.py +233 -0
- crewai_suno-0.1.0/src/suno_crewai/tools.py +75 -0
- crewai_suno-0.1.0/tests/contract-source.json +7 -0
- crewai_suno-0.1.0/tests/live-readonly-evidence.json +16 -0
- crewai_suno-0.1.0/tests/test_agent.py +135 -0
- crewai_suno-0.1.0/tests/test_tools.py +191 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ACEDATACLOUD_API_KEY=your_api_consumer_token
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: Test and build
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
pull_request:
|
|
5
|
+
jobs:
|
|
6
|
+
test:
|
|
7
|
+
runs-on: ubuntu-latest
|
|
8
|
+
strategy:
|
|
9
|
+
matrix:
|
|
10
|
+
python: ['3.10', '3.12', '3.13']
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: astral-sh/setup-uv@v6
|
|
14
|
+
with:
|
|
15
|
+
python-version: ${{ matrix.python }}
|
|
16
|
+
- run: uv sync --group dev
|
|
17
|
+
- run: uv run pytest
|
|
18
|
+
- run: uv run ruff check .
|
|
19
|
+
- run: uv build
|
|
20
|
+
- name: Install built wheel in a clean environment
|
|
21
|
+
run: |
|
|
22
|
+
uv venv /tmp/suno-wheel --python ${{ matrix.python }}
|
|
23
|
+
uv pip install --python /tmp/suno-wheel/bin/python dist/crewai_suno-0.1.0-py3-none-any.whl
|
|
24
|
+
/tmp/suno-wheel/bin/python -c 'from suno_crewai import SunoGenerateMusicTool, SunoRetrieveTaskTool; t = SunoRetrieveTaskTool(api_key="fixture"); assert "fixture" not in str(t.model_dump()); assert "task_id" in t.args_schema.model_fields; print("clean wheel import and schema passed")'
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ace Data Cloud
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: crewai-suno
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Suno by Ace Data Cloud: native CrewAI music generation and same-task retrieval tools
|
|
5
|
+
Project-URL: Homepage, https://github.com/AceDataCloud/SunoCrewAI
|
|
6
|
+
Project-URL: Repository, https://github.com/AceDataCloud/SunoCrewAI
|
|
7
|
+
Project-URL: Documentation, https://github.com/AceDataCloud/SunoCrewAI#readme
|
|
8
|
+
Project-URL: Issues, https://github.com/AceDataCloud/SunoCrewAI/issues
|
|
9
|
+
Author-email: Ace Data Cloud Dev <dev@acedata.cloud>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: acedatacloud,agents,audio,crewai,music,suno
|
|
13
|
+
Requires-Python: <3.14,>=3.10
|
|
14
|
+
Requires-Dist: crewai<2,>=1.15.28
|
|
15
|
+
Requires-Dist: httpx<1,>=0.28
|
|
16
|
+
Requires-Dist: pydantic<3,>=2.11
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# Suno by Ace Data Cloud for CrewAI
|
|
20
|
+
|
|
21
|
+
Generate Suno music and retrieve the same asynchronous task from native CrewAI tools. **Ace Data Cloud provides this integration; it is not an official Suno or CrewAI product.**
|
|
22
|
+
|
|
23
|
+
[简体中文](https://github.com/AceDataCloud/SunoCrewAI/blob/v0.1.0-preview.1/readme/README_zh_Hans.md) · [Current models and pricing](https://platform.acedata.cloud/models?utm_source=pypi&utm_medium=referral&utm_campaign=evergreen&utm_content=sunocrewai_package_pricing)
|
|
24
|
+
|
|
25
|
+
## 1. Install the integration
|
|
26
|
+
|
|
27
|
+
This is the standalone **Suno** package, not an all-services bundle. A [GitHub review preview with wheel and source archive](https://github.com/AceDataCloud/SunoCrewAI/releases/tag/v0.1.0-preview.1) is available; implementation remains under review. **No PyPI release or official CrewAI catalog listing is claimed.** Use the exact source commit below, or the hash-pinned wheel command after it. Do not install an attachment merely by tag name:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
uv add 'crewai-suno @ git+https://github.com/AceDataCloud/SunoCrewAI.git@e5eeb04661ed7b9a86e1f2f970a9e7943b027ca2'
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
uv add 'crewai-suno @ https://github.com/AceDataCloud/SunoCrewAI/releases/download/v0.1.0-preview.1/crewai_suno-0.1.0-py3-none-any.whl#sha256=d31998ca7f33f54c9603a0acb0d906f25933e858be6c83babc5a6d0e5a478e51'
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The wheel was built from the exact source SHA above. Its SHA256 is pinned in the URL; if an asset is replaced, installation must fail the digest check. The release page is informational, not the trust anchor.
|
|
38
|
+
|
|
39
|
+
The source SHA identifies the reviewed preview; inspect the [implementation PR](https://github.com/AceDataCloud/SunoCrewAI/pull/1) before adoption. After a verified PyPI release, the standard entry will be `uv add crewai-suno`. Do not interpret a repository or built wheel as an official listing. CrewAI currently requires Python 3.10–3.13; on Intel macOS its mandatory `lancedb` dependency may lack a wheel. Use a supported Linux environment rather than skipping dependencies for production.
|
|
40
|
+
|
|
41
|
+
## 2. Register, sign in and obtain your own Token
|
|
42
|
+
|
|
43
|
+
1. Open [Ace Data Cloud → Applications](https://platform.acedata.cloud/console/applications?utm_source=pypi&utm_medium=referral&utm_campaign=evergreen&utm_content=sunocrewai_package_api_key). Register or sign in. If needed, choose **Initialize General Application**.
|
|
44
|
+
2. Open **General application**. Check **Suno** access, current price and balance before generating. A service-specific application can only use its allowed service.
|
|
45
|
+
3. Copy the selected API key with the copy icon **1**, or choose **Manage Keys** **2** to create a separate key.
|
|
46
|
+
|
|
47
|
+

|
|
48
|
+
|
|
49
|
+
4. Choose **Create**, name the key `CrewAI Suno`, and configure expiration/restrictions as needed. If **Allowed APIs** is enabled, allow both `/suno/audios` and `/suno/tasks`.
|
|
50
|
+
|
|
51
|
+

|
|
52
|
+
|
|
53
|
+
These redacted screenshots are reused unchanged from the Ace SunoDify tutorial, revision `8d3d03570e73324220f111f7fe343da0792a8071`, retrieved 2026-10-18. The historical name `Dify tutorial` in the second screenshot is only an example; use `CrewAI Suno`. No visible real credential is included. See [image sources](https://raw.githubusercontent.com/AceDataCloud/SunoCrewAI/v0.1.0-preview.1/assets/tutorial/source.json).
|
|
54
|
+
|
|
55
|
+
Set the raw API-consumer Token in the environment variable `ACEDATACLOUD_API_KEY` using your secret manager or terminal's secure input. Do not prepend `Bearer`, use a platform management Token, put the key in prompts, or commit `.env` files. Tools also accept `api_key=...` when a secure credential store supplies it; it is excluded from serialization.
|
|
56
|
+
|
|
57
|
+
## 3. Add the named Suno tools to your CrewAI agent
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from crewai import Agent
|
|
61
|
+
from suno_crewai import SunoGenerateMusicTool, SunoRetrieveTaskTool
|
|
62
|
+
|
|
63
|
+
agent = Agent(
|
|
64
|
+
role="Music producer",
|
|
65
|
+
goal="Create the requested music and check its task result",
|
|
66
|
+
backstory="Use Suno through Ace Data Cloud within the user's approved budget.",
|
|
67
|
+
llm=your_configured_llm,
|
|
68
|
+
tools=[SunoGenerateMusicTool(), SunoRetrieveTaskTool()],
|
|
69
|
+
allow_delegation=False,
|
|
70
|
+
max_retry_limit=0,
|
|
71
|
+
)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
These are actual CrewAI `BaseTool` implementations with input schemas and discoverable names **Suno by Ace Data Cloud - Generate Music** and **Suno by Ace Data Cloud - Retrieve Task**. They need no MCP server configuration. Adding a generation tool does not itself submit a request. The caller is responsible for approving the workflow's paid actions. The generation tool defaults to one use per instance to reduce duplicate submissions; this is not a funds cap or permission system.
|
|
75
|
+
|
|
76
|
+
## 4. First use: submit once, then query the same ID
|
|
77
|
+
|
|
78
|
+
First inspect the current [model and price catalog](https://platform.acedata.cloud/models?utm_source=pypi&utm_medium=referral&utm_campaign=evergreen&utm_content=sunocrewai_package_first_use). Select a currently supported Suno model; `chirp-v6` is an example from the current Suno API contract, not a permanent default.
|
|
79
|
+
|
|
80
|
+
A generation invocation is **billable**. Run it only after agreeing the model and budget:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
import json
|
|
84
|
+
from suno_crewai import SunoGenerateMusicTool, SunoRetrieveTaskTool
|
|
85
|
+
|
|
86
|
+
# BILLABLE when run; this README does not execute it.
|
|
87
|
+
submitted = json.loads(SunoGenerateMusicTool().run(
|
|
88
|
+
prompt="A gentle instrumental piano melody for a sunrise, no vocals.",
|
|
89
|
+
model="chirp-v6",
|
|
90
|
+
instrumental=True,
|
|
91
|
+
))
|
|
92
|
+
print(submitted["task_id"])
|
|
93
|
+
|
|
94
|
+
# accepted/pending is not completion. Preserve this same ID.
|
|
95
|
+
result = SunoRetrieveTaskTool().run(task_id=submitted["task_id"], wait_seconds=240)
|
|
96
|
+
print(result)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
A pending result looks like `{"status":"pending","success":false,"task_id":"...","media_urls":[]}`. Do not rerun the generation if pending, timeout or an unknown submission outcome occurs. Query the same ID again. A terminal successful result includes `status=succeeded`, `success=true`, audio URLs and the original `cost`, when returned by the service. Costs are Credits; they are not USD. A link's presence does not verify that it remains playable.
|
|
100
|
+
|
|
101
|
+
For an existing task, run only this **read-only** example:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
uv run python examples/query_existing.py YOUR_EXISTING_TASK_ID
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The ID must belong to your Key. The published payloads are `/suno/audios` with `async=true` and `/suno/tasks` with `action=retrieve,id=...`; model IDs pass through unchanged. No substitute model or automatic paid HTTP retry is added. Bounded polling only queries the same task.
|
|
108
|
+
|
|
109
|
+
## 5. Inspect execution and output
|
|
110
|
+
|
|
111
|
+

|
|
112
|
+
|
|
113
|
+
Official CrewAI reference, downloaded 2026-10-18 from its tracing documentation. The screenshot's historical date marker is `10/1/2025`; its capture version is unspecified. It shows a task timeline and Response output, **not this Suno tool running**, not an Ace result, and not a new GUI promised by this package. Inputs are not expanded in this image. For your own run, inspect the named Suno tool event and its returned task status. See [official tracing guide](https://docs.crewai.com/en/observability/tracing) for optional account/authentication requirements; tracing is not enabled by this package.
|
|
114
|
+
|
|
115
|
+
## Troubleshooting and cost boundaries
|
|
116
|
+
|
|
117
|
+
| Result | Action |
|
|
118
|
+
|---|---|
|
|
119
|
+
| 401 / 403 | Check raw consumer Token, expiry, Suno entitlement, Allowed APIs and balance; inspect the actual business error. A 403 is not always moderation. |
|
|
120
|
+
| 400 | Use current public model ID and schema; this first tool only exposes basic text-to-music. Duration/custom actions are not silently guessed. |
|
|
121
|
+
| pending / empty media list | Query the same task ID; never regenerate to poll. |
|
|
122
|
+
| 429 | Reduce concurrency and wait; the tool makes one HTTP attempt. Your agent/LLM may have separate retry behavior. |
|
|
123
|
+
| Timeout / 5xx | Submission may have reached the service; inspect request history before creating a new job. |
|
|
124
|
+
| ToolFailure | The terminal task failed; failure is structured and non-retryable by default. Preserve task/trace ID for support, never the key. |
|
|
125
|
+
| Media won't open | Verify terminal success and network access to the returned HTTPS audio URL. |
|
|
126
|
+
|
|
127
|
+
The integration code is free; API generation and any configured LLM follow their current prices. `cost` is the original task's Credits cost, not a new charge for querying it. Do not use a test count, install count or trace screenshot as revenue evidence. The tool sends the provided music prompt and consumer Token to `api.acedata.cloud`; only filtered result fields are returned to the agent. Do not submit private content without authorization.
|
|
128
|
+
|
|
129
|
+
[Source](https://github.com/AceDataCloud/SunoCrewAI) · [Issues](https://github.com/AceDataCloud/SunoCrewAI/issues) · dev@acedata.cloud
|
|
130
|
+
|
|
131
|
+
Development: `uv sync --group dev`, `uv run pytest`, `uv run ruff check .`, `uv build`. Tests mock paid HTTP calls. No paid generation is performed by CI.
|
|
132
|
+
|
|
133
|
+
Release safety: HTTP errors retain status without raw bodies/headers; nested explicit failure is terminal before finished_at, and terminal missing audio is not success. Cost output is filtered.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Privacy
|
|
2
|
+
|
|
3
|
+
The tools send user-selected music prompts and the API-consumer Token to `api.acedata.cloud` over HTTPS. The Token is read from an environment variable or explicit secure input and excluded from tool serialization. The package adds no analytics or telemetry and does not enable CrewAI tracing. CrewAI, the configured LLM, and the caller may have separate telemetry or trace settings. Do not place secrets or personal data in music prompts without authorization.
|
|
4
|
+
|
|
5
|
+
Returned fields are limited to task status, the task ID, HTTPS audio result URLs and original cost. Raw request payloads and account/application identities are excluded. Optional task polling queries only the same ID. The caller controls retention, LLM context sharing and logging. Do not publish private media links or keys in bug reports.
|
|
6
|
+
|
|
7
|
+
The API and any configured LLM follow current prices and data handling terms; this package does not accept terms or create an account for the user.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Suno by Ace Data Cloud for CrewAI
|
|
2
|
+
|
|
3
|
+
Generate Suno music and retrieve the same asynchronous task from native CrewAI tools. **Ace Data Cloud provides this integration; it is not an official Suno or CrewAI product.**
|
|
4
|
+
|
|
5
|
+
[简体中文](readme/README_zh_Hans.md) · [Current models and pricing](https://platform.acedata.cloud/models?utm_source=github&utm_medium=referral&utm_campaign=evergreen&utm_content=sunocrewai_readme_pricing)
|
|
6
|
+
|
|
7
|
+
## 1. Install the integration
|
|
8
|
+
|
|
9
|
+
This is the standalone **Suno** package, not an all-services bundle. A [GitHub review preview with wheel and source archive](https://github.com/AceDataCloud/SunoCrewAI/releases/tag/v0.1.0-preview.1) is available; implementation remains under review. **No PyPI release or official CrewAI catalog listing is claimed.** Use the exact source commit below, or the hash-pinned wheel command after it. Do not install an attachment merely by tag name:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
uv add 'crewai-suno @ git+https://github.com/AceDataCloud/SunoCrewAI.git@e5eeb04661ed7b9a86e1f2f970a9e7943b027ca2'
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
uv add 'crewai-suno @ https://github.com/AceDataCloud/SunoCrewAI/releases/download/v0.1.0-preview.1/crewai_suno-0.1.0-py3-none-any.whl#sha256=d31998ca7f33f54c9603a0acb0d906f25933e858be6c83babc5a6d0e5a478e51'
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The wheel was built from the exact source SHA above. Its SHA256 is pinned in the URL; if an asset is replaced, installation must fail the digest check. The release page is informational, not the trust anchor.
|
|
20
|
+
|
|
21
|
+
The source SHA identifies the reviewed preview; inspect the [implementation PR](https://github.com/AceDataCloud/SunoCrewAI/pull/1) before adoption. After a verified PyPI release, the standard entry will be `uv add crewai-suno`. Do not interpret a repository or built wheel as an official listing. CrewAI currently requires Python 3.10–3.13; on Intel macOS its mandatory `lancedb` dependency may lack a wheel. Use a supported Linux environment rather than skipping dependencies for production.
|
|
22
|
+
|
|
23
|
+
## 2. Register, sign in and obtain your own Token
|
|
24
|
+
|
|
25
|
+
1. Open [Ace Data Cloud → Applications](https://platform.acedata.cloud/console/applications?utm_source=github&utm_medium=referral&utm_campaign=evergreen&utm_content=sunocrewai_readme_api_key). Register or sign in. If needed, choose **Initialize General Application**.
|
|
26
|
+
2. Open **General application**. Check **Suno** access, current price and balance before generating. A service-specific application can only use its allowed service.
|
|
27
|
+
3. Copy the selected API key with the copy icon **1**, or choose **Manage Keys** **2** to create a separate key.
|
|
28
|
+
|
|
29
|
+

|
|
30
|
+
|
|
31
|
+
4. Choose **Create**, name the key `CrewAI Suno`, and configure expiration/restrictions as needed. If **Allowed APIs** is enabled, allow both `/suno/audios` and `/suno/tasks`.
|
|
32
|
+
|
|
33
|
+

|
|
34
|
+
|
|
35
|
+
These redacted screenshots are reused unchanged from the Ace SunoDify tutorial, revision `8d3d03570e73324220f111f7fe343da0792a8071`, retrieved 2026-10-18. The historical name `Dify tutorial` in the second screenshot is only an example; use `CrewAI Suno`. No visible real credential is included. See [image sources](assets/tutorial/source.json).
|
|
36
|
+
|
|
37
|
+
Set the raw API-consumer Token in the environment variable `ACEDATACLOUD_API_KEY` using your secret manager or terminal's secure input. Do not prepend `Bearer`, use a platform management Token, put the key in prompts, or commit `.env` files. Tools also accept `api_key=...` when a secure credential store supplies it; it is excluded from serialization.
|
|
38
|
+
|
|
39
|
+
## 3. Add the named Suno tools to your CrewAI agent
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from crewai import Agent
|
|
43
|
+
from suno_crewai import SunoGenerateMusicTool, SunoRetrieveTaskTool
|
|
44
|
+
|
|
45
|
+
agent = Agent(
|
|
46
|
+
role="Music producer",
|
|
47
|
+
goal="Create the requested music and check its task result",
|
|
48
|
+
backstory="Use Suno through Ace Data Cloud within the user's approved budget.",
|
|
49
|
+
llm=your_configured_llm,
|
|
50
|
+
tools=[SunoGenerateMusicTool(), SunoRetrieveTaskTool()],
|
|
51
|
+
allow_delegation=False,
|
|
52
|
+
max_retry_limit=0,
|
|
53
|
+
)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
These are actual CrewAI `BaseTool` implementations with input schemas and discoverable names **Suno by Ace Data Cloud - Generate Music** and **Suno by Ace Data Cloud - Retrieve Task**. They need no MCP server configuration. Adding a generation tool does not itself submit a request. The caller is responsible for approving the workflow's paid actions. The generation tool defaults to one use per instance to reduce duplicate submissions; this is not a funds cap or permission system.
|
|
57
|
+
|
|
58
|
+
## 4. First use: submit once, then query the same ID
|
|
59
|
+
|
|
60
|
+
First inspect the current [model and price catalog](https://platform.acedata.cloud/models?utm_source=github&utm_medium=referral&utm_campaign=evergreen&utm_content=sunocrewai_readme_first_use). Select a currently supported Suno model; `chirp-v6` is an example from the current Suno API contract, not a permanent default.
|
|
61
|
+
|
|
62
|
+
A generation invocation is **billable**. Run it only after agreeing the model and budget:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
import json
|
|
66
|
+
from suno_crewai import SunoGenerateMusicTool, SunoRetrieveTaskTool
|
|
67
|
+
|
|
68
|
+
# BILLABLE when run; this README does not execute it.
|
|
69
|
+
submitted = json.loads(SunoGenerateMusicTool().run(
|
|
70
|
+
prompt="A gentle instrumental piano melody for a sunrise, no vocals.",
|
|
71
|
+
model="chirp-v6",
|
|
72
|
+
instrumental=True,
|
|
73
|
+
))
|
|
74
|
+
print(submitted["task_id"])
|
|
75
|
+
|
|
76
|
+
# accepted/pending is not completion. Preserve this same ID.
|
|
77
|
+
result = SunoRetrieveTaskTool().run(task_id=submitted["task_id"], wait_seconds=240)
|
|
78
|
+
print(result)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
A pending result looks like `{"status":"pending","success":false,"task_id":"...","media_urls":[]}`. Do not rerun the generation if pending, timeout or an unknown submission outcome occurs. Query the same ID again. A terminal successful result includes `status=succeeded`, `success=true`, audio URLs and the original `cost`, when returned by the service. Costs are Credits; they are not USD. A link's presence does not verify that it remains playable.
|
|
82
|
+
|
|
83
|
+
For an existing task, run only this **read-only** example:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
uv run python examples/query_existing.py YOUR_EXISTING_TASK_ID
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The ID must belong to your Key. The published payloads are `/suno/audios` with `async=true` and `/suno/tasks` with `action=retrieve,id=...`; model IDs pass through unchanged. No substitute model or automatic paid HTTP retry is added. Bounded polling only queries the same task.
|
|
90
|
+
|
|
91
|
+
## 5. Inspect execution and output
|
|
92
|
+
|
|
93
|
+

|
|
94
|
+
|
|
95
|
+
Official CrewAI reference, downloaded 2026-10-18 from its tracing documentation. The screenshot's historical date marker is `10/1/2025`; its capture version is unspecified. It shows a task timeline and Response output, **not this Suno tool running**, not an Ace result, and not a new GUI promised by this package. Inputs are not expanded in this image. For your own run, inspect the named Suno tool event and its returned task status. See [official tracing guide](https://docs.crewai.com/en/observability/tracing) for optional account/authentication requirements; tracing is not enabled by this package.
|
|
96
|
+
|
|
97
|
+
## Troubleshooting and cost boundaries
|
|
98
|
+
|
|
99
|
+
| Result | Action |
|
|
100
|
+
|---|---|
|
|
101
|
+
| 401 / 403 | Check raw consumer Token, expiry, Suno entitlement, Allowed APIs and balance; inspect the actual business error. A 403 is not always moderation. |
|
|
102
|
+
| 400 | Use current public model ID and schema; this first tool only exposes basic text-to-music. Duration/custom actions are not silently guessed. |
|
|
103
|
+
| pending / empty media list | Query the same task ID; never regenerate to poll. |
|
|
104
|
+
| 429 | Reduce concurrency and wait; the tool makes one HTTP attempt. Your agent/LLM may have separate retry behavior. |
|
|
105
|
+
| Timeout / 5xx | Submission may have reached the service; inspect request history before creating a new job. |
|
|
106
|
+
| ToolFailure | The terminal task failed; failure is structured and non-retryable by default. Preserve task/trace ID for support, never the key. |
|
|
107
|
+
| Media won't open | Verify terminal success and network access to the returned HTTPS audio URL. |
|
|
108
|
+
|
|
109
|
+
The integration code is free; API generation and any configured LLM follow their current prices. `cost` is the original task's Credits cost, not a new charge for querying it. Do not use a test count, install count or trace screenshot as revenue evidence. The tool sends the provided music prompt and consumer Token to `api.acedata.cloud`; only filtered result fields are returned to the agent. Do not submit private content without authorization.
|
|
110
|
+
|
|
111
|
+
[Source](https://github.com/AceDataCloud/SunoCrewAI) · [Issues](https://github.com/AceDataCloud/SunoCrewAI/issues) · dev@acedata.cloud
|
|
112
|
+
|
|
113
|
+
Development: `uv sync --group dev`, `uv run pytest`, `uv run ruff check .`, `uv build`. Tests mock paid HTTP calls. No paid generation is performed by CI.
|
|
114
|
+
|
|
115
|
+
Release safety: HTTP errors retain only status and a safe message, never raw service bodies or credential-bearing headers. Explicit nested failed states terminate polling even before finished_at appears; a finished task without a valid audio URL is not success. Original cost output is restricted to Credits amount/list_amount/currency.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Suno by Ace Data Cloud for CrewAI
|
|
2
|
+
|
|
3
|
+
Generate Suno music and retrieve the same asynchronous task from native CrewAI tools. **Ace Data Cloud provides this integration; it is not an official Suno or CrewAI product.**
|
|
4
|
+
|
|
5
|
+
[简体中文](https://github.com/AceDataCloud/SunoCrewAI/blob/v0.1.0-preview.1/readme/README_zh_Hans.md) · [Current models and pricing](https://platform.acedata.cloud/models?utm_source=pypi&utm_medium=referral&utm_campaign=evergreen&utm_content=sunocrewai_package_pricing)
|
|
6
|
+
|
|
7
|
+
## 1. Install the integration
|
|
8
|
+
|
|
9
|
+
This is the standalone **Suno** package, not an all-services bundle. A [GitHub review preview with wheel and source archive](https://github.com/AceDataCloud/SunoCrewAI/releases/tag/v0.1.0-preview.1) is available; implementation remains under review. **No PyPI release or official CrewAI catalog listing is claimed.** Use the exact source commit below, or the hash-pinned wheel command after it. Do not install an attachment merely by tag name:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
uv add 'crewai-suno @ git+https://github.com/AceDataCloud/SunoCrewAI.git@e5eeb04661ed7b9a86e1f2f970a9e7943b027ca2'
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
uv add 'crewai-suno @ https://github.com/AceDataCloud/SunoCrewAI/releases/download/v0.1.0-preview.1/crewai_suno-0.1.0-py3-none-any.whl#sha256=d31998ca7f33f54c9603a0acb0d906f25933e858be6c83babc5a6d0e5a478e51'
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The wheel was built from the exact source SHA above. Its SHA256 is pinned in the URL; if an asset is replaced, installation must fail the digest check. The release page is informational, not the trust anchor.
|
|
20
|
+
|
|
21
|
+
The source SHA identifies the reviewed preview; inspect the [implementation PR](https://github.com/AceDataCloud/SunoCrewAI/pull/1) before adoption. After a verified PyPI release, the standard entry will be `uv add crewai-suno`. Do not interpret a repository or built wheel as an official listing. CrewAI currently requires Python 3.10–3.13; on Intel macOS its mandatory `lancedb` dependency may lack a wheel. Use a supported Linux environment rather than skipping dependencies for production.
|
|
22
|
+
|
|
23
|
+
## 2. Register, sign in and obtain your own Token
|
|
24
|
+
|
|
25
|
+
1. Open [Ace Data Cloud → Applications](https://platform.acedata.cloud/console/applications?utm_source=pypi&utm_medium=referral&utm_campaign=evergreen&utm_content=sunocrewai_package_api_key). Register or sign in. If needed, choose **Initialize General Application**.
|
|
26
|
+
2. Open **General application**. Check **Suno** access, current price and balance before generating. A service-specific application can only use its allowed service.
|
|
27
|
+
3. Copy the selected API key with the copy icon **1**, or choose **Manage Keys** **2** to create a separate key.
|
|
28
|
+
|
|
29
|
+

|
|
30
|
+
|
|
31
|
+
4. Choose **Create**, name the key `CrewAI Suno`, and configure expiration/restrictions as needed. If **Allowed APIs** is enabled, allow both `/suno/audios` and `/suno/tasks`.
|
|
32
|
+
|
|
33
|
+

|
|
34
|
+
|
|
35
|
+
These redacted screenshots are reused unchanged from the Ace SunoDify tutorial, revision `8d3d03570e73324220f111f7fe343da0792a8071`, retrieved 2026-10-18. The historical name `Dify tutorial` in the second screenshot is only an example; use `CrewAI Suno`. No visible real credential is included. See [image sources](https://raw.githubusercontent.com/AceDataCloud/SunoCrewAI/v0.1.0-preview.1/assets/tutorial/source.json).
|
|
36
|
+
|
|
37
|
+
Set the raw API-consumer Token in the environment variable `ACEDATACLOUD_API_KEY` using your secret manager or terminal's secure input. Do not prepend `Bearer`, use a platform management Token, put the key in prompts, or commit `.env` files. Tools also accept `api_key=...` when a secure credential store supplies it; it is excluded from serialization.
|
|
38
|
+
|
|
39
|
+
## 3. Add the named Suno tools to your CrewAI agent
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from crewai import Agent
|
|
43
|
+
from suno_crewai import SunoGenerateMusicTool, SunoRetrieveTaskTool
|
|
44
|
+
|
|
45
|
+
agent = Agent(
|
|
46
|
+
role="Music producer",
|
|
47
|
+
goal="Create the requested music and check its task result",
|
|
48
|
+
backstory="Use Suno through Ace Data Cloud within the user's approved budget.",
|
|
49
|
+
llm=your_configured_llm,
|
|
50
|
+
tools=[SunoGenerateMusicTool(), SunoRetrieveTaskTool()],
|
|
51
|
+
allow_delegation=False,
|
|
52
|
+
max_retry_limit=0,
|
|
53
|
+
)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
These are actual CrewAI `BaseTool` implementations with input schemas and discoverable names **Suno by Ace Data Cloud - Generate Music** and **Suno by Ace Data Cloud - Retrieve Task**. They need no MCP server configuration. Adding a generation tool does not itself submit a request. The caller is responsible for approving the workflow's paid actions. The generation tool defaults to one use per instance to reduce duplicate submissions; this is not a funds cap or permission system.
|
|
57
|
+
|
|
58
|
+
## 4. First use: submit once, then query the same ID
|
|
59
|
+
|
|
60
|
+
First inspect the current [model and price catalog](https://platform.acedata.cloud/models?utm_source=pypi&utm_medium=referral&utm_campaign=evergreen&utm_content=sunocrewai_package_first_use). Select a currently supported Suno model; `chirp-v6` is an example from the current Suno API contract, not a permanent default.
|
|
61
|
+
|
|
62
|
+
A generation invocation is **billable**. Run it only after agreeing the model and budget:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
import json
|
|
66
|
+
from suno_crewai import SunoGenerateMusicTool, SunoRetrieveTaskTool
|
|
67
|
+
|
|
68
|
+
# BILLABLE when run; this README does not execute it.
|
|
69
|
+
submitted = json.loads(SunoGenerateMusicTool().run(
|
|
70
|
+
prompt="A gentle instrumental piano melody for a sunrise, no vocals.",
|
|
71
|
+
model="chirp-v6",
|
|
72
|
+
instrumental=True,
|
|
73
|
+
))
|
|
74
|
+
print(submitted["task_id"])
|
|
75
|
+
|
|
76
|
+
# accepted/pending is not completion. Preserve this same ID.
|
|
77
|
+
result = SunoRetrieveTaskTool().run(task_id=submitted["task_id"], wait_seconds=240)
|
|
78
|
+
print(result)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
A pending result looks like `{"status":"pending","success":false,"task_id":"...","media_urls":[]}`. Do not rerun the generation if pending, timeout or an unknown submission outcome occurs. Query the same ID again. A terminal successful result includes `status=succeeded`, `success=true`, audio URLs and the original `cost`, when returned by the service. Costs are Credits; they are not USD. A link's presence does not verify that it remains playable.
|
|
82
|
+
|
|
83
|
+
For an existing task, run only this **read-only** example:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
uv run python examples/query_existing.py YOUR_EXISTING_TASK_ID
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The ID must belong to your Key. The published payloads are `/suno/audios` with `async=true` and `/suno/tasks` with `action=retrieve,id=...`; model IDs pass through unchanged. No substitute model or automatic paid HTTP retry is added. Bounded polling only queries the same task.
|
|
90
|
+
|
|
91
|
+
## 5. Inspect execution and output
|
|
92
|
+
|
|
93
|
+

|
|
94
|
+
|
|
95
|
+
Official CrewAI reference, downloaded 2026-10-18 from its tracing documentation. The screenshot's historical date marker is `10/1/2025`; its capture version is unspecified. It shows a task timeline and Response output, **not this Suno tool running**, not an Ace result, and not a new GUI promised by this package. Inputs are not expanded in this image. For your own run, inspect the named Suno tool event and its returned task status. See [official tracing guide](https://docs.crewai.com/en/observability/tracing) for optional account/authentication requirements; tracing is not enabled by this package.
|
|
96
|
+
|
|
97
|
+
## Troubleshooting and cost boundaries
|
|
98
|
+
|
|
99
|
+
| Result | Action |
|
|
100
|
+
|---|---|
|
|
101
|
+
| 401 / 403 | Check raw consumer Token, expiry, Suno entitlement, Allowed APIs and balance; inspect the actual business error. A 403 is not always moderation. |
|
|
102
|
+
| 400 | Use current public model ID and schema; this first tool only exposes basic text-to-music. Duration/custom actions are not silently guessed. |
|
|
103
|
+
| pending / empty media list | Query the same task ID; never regenerate to poll. |
|
|
104
|
+
| 429 | Reduce concurrency and wait; the tool makes one HTTP attempt. Your agent/LLM may have separate retry behavior. |
|
|
105
|
+
| Timeout / 5xx | Submission may have reached the service; inspect request history before creating a new job. |
|
|
106
|
+
| ToolFailure | The terminal task failed; failure is structured and non-retryable by default. Preserve task/trace ID for support, never the key. |
|
|
107
|
+
| Media won't open | Verify terminal success and network access to the returned HTTPS audio URL. |
|
|
108
|
+
|
|
109
|
+
The integration code is free; API generation and any configured LLM follow their current prices. `cost` is the original task's Credits cost, not a new charge for querying it. Do not use a test count, install count or trace screenshot as revenue evidence. The tool sends the provided music prompt and consumer Token to `api.acedata.cloud`; only filtered result fields are returned to the agent. Do not submit private content without authorization.
|
|
110
|
+
|
|
111
|
+
[Source](https://github.com/AceDataCloud/SunoCrewAI) · [Issues](https://github.com/AceDataCloud/SunoCrewAI/issues) · dev@acedata.cloud
|
|
112
|
+
|
|
113
|
+
Development: `uv sync --group dev`, `uv run pytest`, `uv run ruff check .`, `uv build`. Tests mock paid HTTP calls. No paid generation is performed by CI.
|
|
114
|
+
|
|
115
|
+
Release safety: HTTP errors retain status without raw bodies/headers; nested explicit failure is terminal before finished_at, and terminal missing audio is not success. Cost output is filtered.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"retrieved_at": "2026-10-18",
|
|
3
|
+
"images": [
|
|
4
|
+
{"file":"get-api-key-en.png","source_url":"https://raw.githubusercontent.com/AceDataCloud/SunoDify/8d3d03570e73324220f111f7fe343da0792a8071/_assets/tutorial/get-api-key-en.png","what_it_shows":"Redacted General application API Key copy and Manage Keys controls","limits":"Historical screenshot; no credentials visible; reused unchanged"},
|
|
5
|
+
{"file":"create-api-key-en.png","source_url":"https://raw.githubusercontent.com/AceDataCloud/SunoDify/8d3d03570e73324220f111f7fe343da0792a8071/_assets/tutorial/create-api-key-en.png","what_it_shows":"Create credential dialog","limits":"Historical example says Dify tutorial; README explains to use CrewAI Suno; reused unchanged"},
|
|
6
|
+
{"file":"official-crewai-tracing.png","source_url":"https://raw.githubusercontent.com/crewAIInc/crewAI/40bb2d3b586e8367d206090cc62df5242206008f/docs/images/crewai-tracing.png","official_page_url":"https://docs.crewai.com/v1.15.28/en/observability/tracing","what_it_shows":"Official CrewAI execution timeline and Response output","limits":"Historical date marker 10/1/2025; unknown capture version; not Ace runtime, not Suno package validation, not promised GUI"}
|
|
7
|
+
]
|
|
8
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Free read-only example; requires a task already owned by your API Key."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
from suno_crewai import SunoRetrieveTaskTool
|
|
6
|
+
|
|
7
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
8
|
+
parser.add_argument("task_id")
|
|
9
|
+
args = parser.parse_args()
|
|
10
|
+
print(SunoRetrieveTaskTool().run(task_id=args.task_id, wait_seconds=0))
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "crewai-suno"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Suno by Ace Data Cloud: native CrewAI music generation and same-task retrieval tools"
|
|
5
|
+
readme = "README.pypi.md"
|
|
6
|
+
requires-python = ">=3.10,<3.14"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
authors = [{name = "Ace Data Cloud Dev", email = "dev@acedata.cloud"}]
|
|
9
|
+
keywords = ["crewai", "suno", "music", "audio", "agents", "acedatacloud"]
|
|
10
|
+
dependencies = ["crewai>=1.15.28,<2", "httpx>=0.28,<1", "pydantic>=2.11,<3"]
|
|
11
|
+
|
|
12
|
+
[project.urls]
|
|
13
|
+
Homepage = "https://github.com/AceDataCloud/SunoCrewAI"
|
|
14
|
+
Repository = "https://github.com/AceDataCloud/SunoCrewAI"
|
|
15
|
+
Documentation = "https://github.com/AceDataCloud/SunoCrewAI#readme"
|
|
16
|
+
Issues = "https://github.com/AceDataCloud/SunoCrewAI/issues"
|
|
17
|
+
|
|
18
|
+
[dependency-groups]
|
|
19
|
+
dev = ["pytest>=8,<10", "ruff==0.15.1", "build>=1,<2", "twine>=6,<7"]
|
|
20
|
+
|
|
21
|
+
[build-system]
|
|
22
|
+
requires = ["hatchling"]
|
|
23
|
+
build-backend = "hatchling.build"
|
|
24
|
+
|
|
25
|
+
[tool.hatch.build.targets.wheel]
|
|
26
|
+
packages = ["src/suno_crewai"]
|
|
27
|
+
|
|
28
|
+
[tool.pytest.ini_options]
|
|
29
|
+
testpaths = ["tests"]
|
|
30
|
+
|
|
31
|
+
[tool.ruff]
|
|
32
|
+
target-version = "py310"
|
|
33
|
+
|
|
34
|
+
[tool.ruff.lint]
|
|
35
|
+
select = ["E", "F", "I", "B", "UP"]
|
|
36
|
+
ignore = ["E501"]
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Suno by Ace Data Cloud:CrewAI 原生音乐工具
|
|
2
|
+
|
|
3
|
+
本集成由 **Ace Data Cloud** 提供,不是 Suno 或 CrewAI 官方产品。独立 Suno 品牌包,不是 17 服务合集。[English](../README.md)
|
|
4
|
+
|
|
5
|
+
## 安装入口
|
|
6
|
+
|
|
7
|
+
已有 [GitHub 预览 wheel 与源码包](https://github.com/AceDataCloud/SunoCrewAI/releases/tag/v0.1.0-preview.1),源码仍在审阅,**尚未 PyPI 发布或 CrewAI 官方目录收录**。先审阅[本仓库 PR](https://github.com/AceDataCloud/SunoCrewAI/pulls),固定下列源码,或使用带 SHA256 校验的 wheel;不要仅按 tag 名称安装附件:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
uv add 'crewai-suno @ git+https://github.com/AceDataCloud/SunoCrewAI.git@e5eeb04661ed7b9a86e1f2f970a9e7943b027ca2'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
uv add 'crewai-suno @ https://github.com/AceDataCloud/SunoCrewAI/releases/download/v0.1.0-preview.1/crewai_suno-0.1.0-py3-none-any.whl#sha256=d31998ca7f33f54c9603a0acb0d906f25933e858be6c83babc5a6d0e5a478e51'
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
wheel 来自同一固定源码 SHA,URL 强制校验 SHA256;若附件替换,摘要不符必须失败。release 页面仅提供信息,不作为信任锚。
|
|
18
|
+
|
|
19
|
+
发布后目标命令为 `uv add crewai-suno`,尚未发布前不要把它当现成安装入口。CrewAI 要求 Python 3.10–3.13;Intel macOS 可能缺 lancedb wheel,正式使用请选择支持的 Linux 环境,不跳过依赖冒充完整安装。
|
|
20
|
+
|
|
21
|
+
## 获取自己的 API Token
|
|
22
|
+
|
|
23
|
+
1. 打开 [Ace Applications](https://platform.acedata.cloud/console/applications?utm_source=github&utm_medium=referral&utm_campaign=evergreen&utm_content=sunocrewai_readme_zh_api_key),注册并登录。必要时选择 **Initialize General Application**。
|
|
24
|
+
2. 打开 **General application**,确认 Suno 权限、当前余额和[价格](https://platform.acedata.cloud/models?utm_source=github&utm_medium=referral&utm_campaign=evergreen&utm_content=sunocrewai_readme_zh_pricing)。服务专属应用只支持授权的服务。
|
|
25
|
+
3. 点击下图 **1** 复制已有 Key,或 **2 Manage Keys → Create** 创建独立 Key。
|
|
26
|
+
|
|
27
|
+

|
|
28
|
+
|
|
29
|
+
4. 将新 Key 命名为 `CrewAI Suno`,按需配置过期、额度和权限。若启用 **Allowed APIs**,同时允许 `/suno/audios` 与 `/suno/tasks`。
|
|
30
|
+
|
|
31
|
+

|
|
32
|
+
|
|
33
|
+
图片于 2026-10-18 从 SunoDify 固定版本原样复用,来源见 [source.json](../assets/tutorial/source.json)。第二图历史名称是 `Dify tutorial`,这里只是示例,请填写 `CrewAI Suno`。没有可见真实凭据。
|
|
34
|
+
|
|
35
|
+
用安全输入或密钥管理器设置环境变量 `ACEDATACLOUD_API_KEY`。只放 Token 原文,不加 `Bearer`、引号或空格;不要用 platform 管理 Token,也不要把 Key 放进 prompt 或提交到 `.env`。工具可从安全凭据存储接收 `api_key=...`,序列化不包含它。
|
|
36
|
+
|
|
37
|
+
## 在 CrewAI 中添加 Suno 工具
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from crewai import Agent
|
|
41
|
+
from suno_crewai import SunoGenerateMusicTool, SunoRetrieveTaskTool
|
|
42
|
+
|
|
43
|
+
agent = Agent(
|
|
44
|
+
role="音乐制作人",
|
|
45
|
+
goal="按用户已批准预算生成音乐,并查询同一任务结果",
|
|
46
|
+
backstory="使用 Ace Data Cloud 的 Suno 服务",
|
|
47
|
+
llm=your_configured_llm,
|
|
48
|
+
tools=[SunoGenerateMusicTool(), SunoRetrieveTaskTool()],
|
|
49
|
+
allow_delegation=False,
|
|
50
|
+
max_retry_limit=0,
|
|
51
|
+
)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
工具名称为 **Suno by Ace Data Cloud - Generate Music** 和 **Suno by Ace Data Cloud - Retrieve Task**,有原生输入 schema,无需配置 MCP。只是添加工具不会调用接口;调用者负责批准付费行为。生成工具每个实例默认只允许一次调用,防止重复提交,但这不是额度上限或权限系统。
|
|
55
|
+
|
|
56
|
+
## 首用与同 ID 查询
|
|
57
|
+
|
|
58
|
+
先查当前模型目录,选择公开 Suno model ID;`chirp-v6` 是当前 API 合同示例,不是永久默认。生成请求收费,须先确定模型与额度:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
import json
|
|
62
|
+
from suno_crewai import SunoGenerateMusicTool, SunoRetrieveTaskTool
|
|
63
|
+
|
|
64
|
+
# 实际执行会收费;本教程没有运行该调用。
|
|
65
|
+
submitted = json.loads(SunoGenerateMusicTool().run(
|
|
66
|
+
prompt="A gentle instrumental piano melody for a sunrise, no vocals.",
|
|
67
|
+
model="chirp-v6", instrumental=True,
|
|
68
|
+
))
|
|
69
|
+
result = SunoRetrieveTaskTool().run(task_id=submitted["task_id"], wait_seconds=240)
|
|
70
|
+
print(result)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
accepted / pending 不等于成功;保存原 `task_id`,未完成、超时或提交结果不明时,不要重跑生成。继续查询同一 ID。成功返回 `status=succeeded`、`success=true`、`media_urls` 及接口返回的原 `cost`(Credits,不是 USD)。媒体链接存在不代表已验证可播放。
|
|
74
|
+
|
|
75
|
+
对用户自己的旧任务,可直接免费只读查询:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
uv run python examples/query_existing.py YOUR_EXISTING_TASK_ID
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
实际接口是 `/suno/audios` + `async=true`,以及 `/suno/tasks` + `action=retrieve,id=...`。只做初始文字生成,不猜测 duration 或高级动作,不加模型 fallback,不自动重试收费 HTTP 提交;有界等待只查询同一个任务。
|
|
82
|
+
|
|
83
|
+
## 在真实 CrewAI 界面查看执行
|
|
84
|
+
|
|
85
|
+

|
|
86
|
+
|
|
87
|
+
图为 CrewAI 官方历史原图,2026-10-18 下载,原图日期标记 `10/1/2025`、版本未注明;左侧是执行步骤,右侧展开 Response,输入未展开。**不是本 Suno 包运行截图,不证明 Ace 上线,也不承诺新增 GUI**。你的实际执行应检查具名 Suno 工具事件和 task 状态;可选 tracing 的账号/认证见[官方文档](https://docs.crewai.com/en/observability/tracing),本包不自动开启追踪。
|
|
88
|
+
|
|
89
|
+
## 常见问题
|
|
90
|
+
|
|
91
|
+
| 情况 | 处理 |
|
|
92
|
+
|---|---|
|
|
93
|
+
| 401 / 403 | 检查完整消费者 Token、过期、Suno 权限、Allowed APIs 和余额;403 不一概等于内容审核。 |
|
|
94
|
+
| 400 | 对照当前模型及输入 schema;本工具不隐式支持所有高级动作。 |
|
|
95
|
+
| pending / 无结果 | 只查同一任务 ID,不重新生成。 |
|
|
96
|
+
| 429 | 降低并发并等待;工具只发一次 HTTP,LLM / agent 自身可能另有重试。 |
|
|
97
|
+
| 超时 / 5xx | 提交可能已到服务端,先看请求历史再决定是否重新生成。 |
|
|
98
|
+
| ToolFailure | 已明确失败;结构化错误默认不可重试。保留 task/trace ID 联系支持,不提供 Key。 |
|
|
99
|
+
| 音频无法打开 | 确认终态成功,再检查返回 HTTPS URL 的网络访问。 |
|
|
100
|
+
|
|
101
|
+
代码免费,生成和你配置的 LLM 按当前价格计费;查询中的 cost 是原任务费用,不是新增营收或新扣款证据。工具把用户 prompt 与 Token 发往 `api.acedata.cloud`,仅向 agent 返回过滤后的结果,不返回原始请求和身份字段。测试均 Mock 收费 HTTP,不把安装、测试次数当用户或收入。
|
|
102
|
+
|
|
103
|
+
[源码](https://github.com/AceDataCloud/SunoCrewAI) · [反馈](https://github.com/AceDataCloud/SunoCrewAI/issues) · dev@acedata.cloud
|
|
104
|
+
|
|
105
|
+
发布安全:HTTP错误仅保留状态与安全说明,不返回原始服务body或凭据headers;嵌套failed等状态在finished_at尚未写入时也停止轮询。终态缺有效音频URL不算成功;原费用只保留Credits的amount/list_amount/currency。
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""Single-attempt consumer-token transport and strict asynchronous task results.
|
|
2
|
+
|
|
3
|
+
Shared safety semantics with the independently reviewed media packages.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import time
|
|
11
|
+
from typing import Any, ClassVar
|
|
12
|
+
from urllib.parse import urlsplit
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
from crewai.tools import BaseTool, EnvVar
|
|
16
|
+
from crewai.tools.tool_failure import ToolFailure, ToolFailureReason
|
|
17
|
+
from pydantic import BaseModel, Field, PrivateAttr
|
|
18
|
+
|
|
19
|
+
API_BASE = "https://api.acedata.cloud"
|
|
20
|
+
FAILED = {"failed", "error", "cancelled", "canceled", "rejected"}
|
|
21
|
+
PENDING = {"pending", "queued", "processing", "running", "submitted", "created"}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ServiceHTTPError(RuntimeError):
|
|
25
|
+
"""Preserve HTTP meaning without exposing response bodies or request headers."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, status_code: int) -> None:
|
|
28
|
+
self.status_code = status_code
|
|
29
|
+
super().__init__(
|
|
30
|
+
f"Service HTTP {status_code}; inspect the request using its task/trace ID. Do not include your Token."
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class RetrieveTaskInput(BaseModel):
|
|
35
|
+
task_id: str = Field(
|
|
36
|
+
min_length=1,
|
|
37
|
+
description="The original task_id from generation; never create another job to poll",
|
|
38
|
+
)
|
|
39
|
+
wait_seconds: int = Field(
|
|
40
|
+
default=0, ge=0, le=240, description="Bounded same-ID polling; 0 queries once"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ServiceTool(BaseTool):
|
|
45
|
+
_api_key: str = PrivateAttr()
|
|
46
|
+
service: ClassVar[str]
|
|
47
|
+
task_path: ClassVar[str]
|
|
48
|
+
env_vars: list[EnvVar] = Field(
|
|
49
|
+
default_factory=lambda: [
|
|
50
|
+
EnvVar(
|
|
51
|
+
name="ACEDATACLOUD_API_KEY",
|
|
52
|
+
description="Raw Ace API-consumer Token with the selected service access",
|
|
53
|
+
required=True,
|
|
54
|
+
)
|
|
55
|
+
]
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def __init__(self, api_key: str | None = None, **kwargs: Any) -> None:
|
|
59
|
+
super().__init__(**kwargs)
|
|
60
|
+
token = api_key or os.environ.get("ACEDATACLOUD_API_KEY", "")
|
|
61
|
+
if (
|
|
62
|
+
not token
|
|
63
|
+
or token != token.strip()
|
|
64
|
+
or token.lower().startswith(("bearer ", "platform-"))
|
|
65
|
+
):
|
|
66
|
+
raise ValueError(
|
|
67
|
+
"Set ACEDATACLOUD_API_KEY to the raw API-consumer Token, without Bearer, spaces or platform management credentials"
|
|
68
|
+
)
|
|
69
|
+
self._api_key = token
|
|
70
|
+
|
|
71
|
+
def _request(self, path: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
72
|
+
try:
|
|
73
|
+
with httpx.Client(timeout=45, follow_redirects=False) as client:
|
|
74
|
+
response = client.post(
|
|
75
|
+
API_BASE + path,
|
|
76
|
+
headers={
|
|
77
|
+
"Authorization": f"Bearer {self._api_key}",
|
|
78
|
+
"Accept": "application/json",
|
|
79
|
+
},
|
|
80
|
+
json=payload,
|
|
81
|
+
)
|
|
82
|
+
if response.status_code >= 300:
|
|
83
|
+
raise ServiceHTTPError(response.status_code)
|
|
84
|
+
try:
|
|
85
|
+
body = response.json()
|
|
86
|
+
except ValueError:
|
|
87
|
+
raise ValueError(
|
|
88
|
+
"Invalid service JSON; check request history before submitting again"
|
|
89
|
+
) from None
|
|
90
|
+
except httpx.TimeoutException:
|
|
91
|
+
raise TimeoutError(
|
|
92
|
+
"Request timed out; submission outcome may be unknown. Inspect request history before any resubmission."
|
|
93
|
+
) from None
|
|
94
|
+
except httpx.TransportError:
|
|
95
|
+
raise ConnectionError(
|
|
96
|
+
"Service connection failed; inspect request history before any resubmission."
|
|
97
|
+
) from None
|
|
98
|
+
if not isinstance(body, dict):
|
|
99
|
+
raise ValueError(
|
|
100
|
+
"Invalid service response shape; check request history before submitting again"
|
|
101
|
+
)
|
|
102
|
+
return body
|
|
103
|
+
|
|
104
|
+
def _failure(self, task_id: str, code: str = "task_failed") -> ToolFailure:
|
|
105
|
+
return ToolFailure(
|
|
106
|
+
message=f"{self.service} task failed or returned an invalid terminal result; inspect its task/trace ID before resubmitting.",
|
|
107
|
+
reason=ToolFailureReason.TOOL_REPORTED,
|
|
108
|
+
code=code,
|
|
109
|
+
retryable=False,
|
|
110
|
+
details={"task_id": task_id},
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def _media_urls(self, result: dict[str, Any]) -> list[str]:
|
|
114
|
+
raise NotImplementedError
|
|
115
|
+
|
|
116
|
+
def _normalize(
|
|
117
|
+
self, body: dict[str, Any], task_id: str, *, retrieved: bool
|
|
118
|
+
) -> dict[str, Any] | ToolFailure:
|
|
119
|
+
if retrieved:
|
|
120
|
+
returned_id = body.get("id") or body.get("task_id")
|
|
121
|
+
if returned_id and returned_id != task_id:
|
|
122
|
+
raise ValueError("Returned task ID does not match the requested task")
|
|
123
|
+
result = body.get("response") if retrieved else body
|
|
124
|
+
if isinstance(result, str):
|
|
125
|
+
try:
|
|
126
|
+
result = json.loads(result)
|
|
127
|
+
except ValueError:
|
|
128
|
+
return self._failure(task_id, "invalid_terminal_result")
|
|
129
|
+
result = result if isinstance(result, dict) else {}
|
|
130
|
+
resolved_id = task_id or str(
|
|
131
|
+
body.get("task_id")
|
|
132
|
+
or (
|
|
133
|
+
body.get("task", {}).get("id")
|
|
134
|
+
if isinstance(body.get("task"), dict)
|
|
135
|
+
else ""
|
|
136
|
+
)
|
|
137
|
+
or ""
|
|
138
|
+
)
|
|
139
|
+
finished = retrieved and bool(body.get("finished_at"))
|
|
140
|
+
# Check service state within data/content, including item lists, before
|
|
141
|
+
# interpreting the envelope success flag. Do not recurse arbitrary fields.
|
|
142
|
+
states: list[str] = [
|
|
143
|
+
str(body[key]).lower()
|
|
144
|
+
for key in ("state", "status")
|
|
145
|
+
if isinstance(body.get(key), str)
|
|
146
|
+
]
|
|
147
|
+
errors = [bool(body.get("error")), bool(result.get("error"))]
|
|
148
|
+
|
|
149
|
+
def inspect(node: Any) -> None:
|
|
150
|
+
if isinstance(node, list):
|
|
151
|
+
for item in node:
|
|
152
|
+
inspect(item)
|
|
153
|
+
elif isinstance(node, dict):
|
|
154
|
+
for key in ("state", "status"):
|
|
155
|
+
if isinstance(node.get(key), str):
|
|
156
|
+
states.append(node[key].lower())
|
|
157
|
+
if node.get("error") or node.get("success") is False:
|
|
158
|
+
errors.append(True)
|
|
159
|
+
for key in ("data", "content", "task"):
|
|
160
|
+
if key in node:
|
|
161
|
+
inspect(node[key])
|
|
162
|
+
|
|
163
|
+
inspect(body if not retrieved else result)
|
|
164
|
+
failed = any(errors) or any(state in FAILED for state in states)
|
|
165
|
+
if (
|
|
166
|
+
any(state in FAILED for state in states)
|
|
167
|
+
or body.get("error")
|
|
168
|
+
or (finished and failed)
|
|
169
|
+
or (not retrieved and failed)
|
|
170
|
+
):
|
|
171
|
+
return self._failure(resolved_id)
|
|
172
|
+
if not resolved_id:
|
|
173
|
+
raise ValueError(
|
|
174
|
+
"No task ID returned; inspect request history before any resubmission"
|
|
175
|
+
)
|
|
176
|
+
succeeded = (
|
|
177
|
+
finished
|
|
178
|
+
and result.get("success") is True
|
|
179
|
+
and not any(s in PENDING for s in states)
|
|
180
|
+
)
|
|
181
|
+
urls = self._media_urls(result) if succeeded else []
|
|
182
|
+
if succeeded and not urls:
|
|
183
|
+
return self._failure(resolved_id, "missing_terminal_media")
|
|
184
|
+
cost = result.get("cost") if succeeded else None
|
|
185
|
+
cost = (
|
|
186
|
+
{
|
|
187
|
+
k: v
|
|
188
|
+
for k, v in cost.items()
|
|
189
|
+
if (
|
|
190
|
+
k in {"amount", "list_amount"}
|
|
191
|
+
and isinstance(v, (int, float))
|
|
192
|
+
and not isinstance(v, bool)
|
|
193
|
+
)
|
|
194
|
+
or (k == "currency" and v == "credit")
|
|
195
|
+
}
|
|
196
|
+
if isinstance(cost, dict)
|
|
197
|
+
else None
|
|
198
|
+
)
|
|
199
|
+
return {
|
|
200
|
+
"status": "succeeded" if succeeded else "pending",
|
|
201
|
+
"success": succeeded,
|
|
202
|
+
"task_id": resolved_id,
|
|
203
|
+
"media_urls": urls,
|
|
204
|
+
"cost": cost,
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
def _retrieve(self, task_id: str, wait_seconds: int = 0) -> str | ToolFailure:
|
|
208
|
+
deadline = time.monotonic() + wait_seconds
|
|
209
|
+
while True:
|
|
210
|
+
body = self._request(self.task_path, {"action": "retrieve", "id": task_id})
|
|
211
|
+
result = self._normalize(body, task_id, retrieved=True)
|
|
212
|
+
if isinstance(result, ToolFailure):
|
|
213
|
+
return result
|
|
214
|
+
remaining = deadline - time.monotonic()
|
|
215
|
+
if result["status"] != "pending" or remaining <= 0:
|
|
216
|
+
return json.dumps(result)
|
|
217
|
+
time.sleep(min(5, remaining))
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def https_urls(values: list[Any]) -> list[str]:
|
|
221
|
+
urls = []
|
|
222
|
+
for value in values:
|
|
223
|
+
if isinstance(value, str):
|
|
224
|
+
parts = urlsplit(value)
|
|
225
|
+
if (
|
|
226
|
+
parts.scheme == "https"
|
|
227
|
+
and parts.hostname
|
|
228
|
+
and not parts.username
|
|
229
|
+
and not parts.password
|
|
230
|
+
and value not in urls
|
|
231
|
+
):
|
|
232
|
+
urls.append(value)
|
|
233
|
+
return urls
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Suno music generation and same-task retrieval for CrewAI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from crewai.tools.tool_failure import ToolFailure
|
|
9
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
10
|
+
|
|
11
|
+
from suno_crewai._base import RetrieveTaskInput, ServiceTool, https_urls
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class GenerateMusicInput(BaseModel):
|
|
15
|
+
model_config = ConfigDict(extra="forbid")
|
|
16
|
+
prompt: str = Field(min_length=1, description="Description of music to generate")
|
|
17
|
+
model: str = Field(
|
|
18
|
+
min_length=1, description="Current public Suno model ID, passed unchanged"
|
|
19
|
+
)
|
|
20
|
+
instrumental: bool = Field(
|
|
21
|
+
default=True, description="Generate instrumental music without vocals"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class _SunoTool(ServiceTool):
|
|
26
|
+
service = "Suno"
|
|
27
|
+
task_path = "/suno/tasks"
|
|
28
|
+
|
|
29
|
+
def _media_urls(self, result: dict[str, Any]) -> list[str]:
|
|
30
|
+
data = result.get("data") or []
|
|
31
|
+
return https_urls(
|
|
32
|
+
[item.get("audio_url") for item in data if isinstance(item, dict)]
|
|
33
|
+
if isinstance(data, list)
|
|
34
|
+
else []
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SunoGenerateMusicTool(_SunoTool):
|
|
39
|
+
name: str = "Suno by Ace Data Cloud - Generate Music"
|
|
40
|
+
description: str = (
|
|
41
|
+
"Generate music with Suno through Ace Data Cloud. This submits one billable job; "
|
|
42
|
+
"accepted is pending, not complete. Save task_id and use Retrieve Task to wait. "
|
|
43
|
+
"Never repeat generation to poll or recover an unknown submission outcome."
|
|
44
|
+
)
|
|
45
|
+
args_schema: type[BaseModel] = GenerateMusicInput
|
|
46
|
+
max_usage_count: int | None = 1
|
|
47
|
+
|
|
48
|
+
def _run(
|
|
49
|
+
self, prompt: str, model: str, instrumental: bool = True
|
|
50
|
+
) -> str | ToolFailure:
|
|
51
|
+
body = self._request(
|
|
52
|
+
"/suno/audios",
|
|
53
|
+
{
|
|
54
|
+
"action": "generate",
|
|
55
|
+
"custom": False,
|
|
56
|
+
"prompt": prompt,
|
|
57
|
+
"model": model,
|
|
58
|
+
"instrumental": instrumental,
|
|
59
|
+
"async": True,
|
|
60
|
+
},
|
|
61
|
+
)
|
|
62
|
+
result = self._normalize(body, "", retrieved=False)
|
|
63
|
+
return result if isinstance(result, ToolFailure) else json.dumps(result)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class SunoRetrieveTaskTool(_SunoTool):
|
|
67
|
+
name: str = "Suno by Ace Data Cloud - Retrieve Task"
|
|
68
|
+
description: str = (
|
|
69
|
+
"Read an existing Suno task_id, optionally wait up to240 seconds. pending is not success. "
|
|
70
|
+
"This tool never creates music; query the same ID if still pending."
|
|
71
|
+
)
|
|
72
|
+
args_schema: type[BaseModel] = RetrieveTaskInput
|
|
73
|
+
|
|
74
|
+
def _run(self, task_id: str, wait_seconds: int = 0) -> str | ToolFailure:
|
|
75
|
+
return self._retrieve(task_id, wait_seconds)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"as_of": "2026-10-18",
|
|
3
|
+
"source": "https://github.com/AceDataCloud/SunoDify/blob/main/tools/api_contracts.py",
|
|
4
|
+
"generation": "POST /suno/audios action=generate custom=false prompt/model/instrumental async=true",
|
|
5
|
+
"retrieve": "POST /suno/tasks action=retrieve id=<same task>",
|
|
6
|
+
"scope": "basic text-to-music and one existing task; no fallback, advanced actions, duration guessing or paid retry"
|
|
7
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"service": "Suno",
|
|
3
|
+
"as_of": "2026-10-18",
|
|
4
|
+
"task_id_matches": true,
|
|
5
|
+
"status": "succeeded",
|
|
6
|
+
"success": true,
|
|
7
|
+
"media_count": 2,
|
|
8
|
+
"original_cost": {
|
|
9
|
+
"amount": 0.504,
|
|
10
|
+
"currency": "credit",
|
|
11
|
+
"list_amount": 0.56
|
|
12
|
+
},
|
|
13
|
+
"new_generation_calls": 0,
|
|
14
|
+
"paid_model_calls": 0,
|
|
15
|
+
"limits": "Read-only existing task, no new bill/unique-bill or media playback verification; runtime missing mandatory lancedb, Linux CI pending."
|
|
16
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""A real CrewAI tool cycle, with a local model fixture and mocked Suno query."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import threading
|
|
5
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
6
|
+
from unittest.mock import patch
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
from crewai import LLM, Agent, Crew, Task
|
|
10
|
+
|
|
11
|
+
from suno_crewai import SunoRetrieveTaskTool
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_agent_queries_existing_suno_task_without_generation(monkeypatch):
|
|
15
|
+
monkeypatch.setenv("CREWAI_TELEMETRY_DISABLED", "true")
|
|
16
|
+
monkeypatch.setenv("OTEL_SDK_DISABLED", "true")
|
|
17
|
+
monkeypatch.setenv("CREWAI_TRACING_ENABLED", "false")
|
|
18
|
+
requests = []
|
|
19
|
+
|
|
20
|
+
class ModelFixture(BaseHTTPRequestHandler):
|
|
21
|
+
def log_message(self, *_args):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
def do_POST(self):
|
|
25
|
+
body = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
|
|
26
|
+
requests.append(body)
|
|
27
|
+
tool_results = [m for m in body["messages"] if m.get("role") == "tool"]
|
|
28
|
+
if tool_results:
|
|
29
|
+
result = json.loads(tool_results[-1]["content"])
|
|
30
|
+
assert result["status"] == "succeeded"
|
|
31
|
+
message = {
|
|
32
|
+
"role": "assistant",
|
|
33
|
+
"content": "Existing Suno task succeeded.",
|
|
34
|
+
}
|
|
35
|
+
finish_reason = "stop"
|
|
36
|
+
else:
|
|
37
|
+
assert len(body["tools"]) == 1
|
|
38
|
+
name = body["tools"][0]["function"]["name"]
|
|
39
|
+
assert "suno" in name.lower() and "retrieve" in name.lower()
|
|
40
|
+
message = {
|
|
41
|
+
"role": "assistant",
|
|
42
|
+
"content": None,
|
|
43
|
+
"tool_calls": [
|
|
44
|
+
{
|
|
45
|
+
"id": "read-only",
|
|
46
|
+
"type": "function",
|
|
47
|
+
"function": {
|
|
48
|
+
"name": name,
|
|
49
|
+
"arguments": json.dumps(
|
|
50
|
+
{"task_id": "existing", "wait_seconds": 0}
|
|
51
|
+
),
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
],
|
|
55
|
+
}
|
|
56
|
+
finish_reason = "tool_calls"
|
|
57
|
+
response = json.dumps(
|
|
58
|
+
{
|
|
59
|
+
"id": "local",
|
|
60
|
+
"object": "chat.completion",
|
|
61
|
+
"created": 1,
|
|
62
|
+
"model": body["model"],
|
|
63
|
+
"choices": [
|
|
64
|
+
{"index": 0, "message": message, "finish_reason": finish_reason}
|
|
65
|
+
],
|
|
66
|
+
"usage": {
|
|
67
|
+
"prompt_tokens": 1,
|
|
68
|
+
"completion_tokens": 1,
|
|
69
|
+
"total_tokens": 2,
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
).encode()
|
|
73
|
+
self.send_response(200)
|
|
74
|
+
self.send_header("Content-Type", "application/json")
|
|
75
|
+
self.send_header("Content-Length", str(len(response)))
|
|
76
|
+
self.end_headers()
|
|
77
|
+
self.wfile.write(response)
|
|
78
|
+
|
|
79
|
+
server = ThreadingHTTPServer(("127.0.0.1", 0), ModelFixture)
|
|
80
|
+
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
81
|
+
thread.start()
|
|
82
|
+
try:
|
|
83
|
+
llm = LLM(
|
|
84
|
+
model="gpt-4.1-mini",
|
|
85
|
+
custom_openai=True,
|
|
86
|
+
base_url=f"http://127.0.0.1:{server.server_port}/v1",
|
|
87
|
+
api_key="local-fixture",
|
|
88
|
+
max_retries=0,
|
|
89
|
+
)
|
|
90
|
+
tool = SunoRetrieveTaskTool(api_key="test-key")
|
|
91
|
+
agent = Agent(
|
|
92
|
+
role="Suno task reader",
|
|
93
|
+
goal="Query the existing task once",
|
|
94
|
+
backstory="Never generate new music",
|
|
95
|
+
tools=[tool],
|
|
96
|
+
llm=llm,
|
|
97
|
+
function_calling_llm=llm,
|
|
98
|
+
allow_delegation=False,
|
|
99
|
+
max_iter=3,
|
|
100
|
+
max_retry_limit=0,
|
|
101
|
+
)
|
|
102
|
+
task = Task(
|
|
103
|
+
description="Query existing task existing and report its state",
|
|
104
|
+
expected_output="Task state",
|
|
105
|
+
agent=agent,
|
|
106
|
+
)
|
|
107
|
+
body = {
|
|
108
|
+
"id": "existing",
|
|
109
|
+
"finished_at": 1,
|
|
110
|
+
"response": {
|
|
111
|
+
"success": True,
|
|
112
|
+
"data": [{"audio_url": "https://example.com/audio.mp3"}],
|
|
113
|
+
},
|
|
114
|
+
}
|
|
115
|
+
response = httpx.Response(
|
|
116
|
+
200,
|
|
117
|
+
json=body,
|
|
118
|
+
request=httpx.Request("POST", "https://api.acedata.cloud/suno/tasks"),
|
|
119
|
+
)
|
|
120
|
+
with patch("httpx.Client.post", return_value=response) as query:
|
|
121
|
+
result = Crew(
|
|
122
|
+
agents=[agent], tasks=[task], memory=False, tracing=False
|
|
123
|
+
).kickoff()
|
|
124
|
+
assert result.raw == "Existing Suno task succeeded."
|
|
125
|
+
assert query.call_count == 1
|
|
126
|
+
assert query.call_args.args[0] == "https://api.acedata.cloud/suno/tasks"
|
|
127
|
+
assert query.call_args.kwargs["json"] == {
|
|
128
|
+
"action": "retrieve",
|
|
129
|
+
"id": "existing",
|
|
130
|
+
}
|
|
131
|
+
assert len(requests) == 2
|
|
132
|
+
finally:
|
|
133
|
+
server.shutdown()
|
|
134
|
+
server.server_close()
|
|
135
|
+
thread.join(timeout=2)
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from unittest.mock import patch
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
import pytest
|
|
6
|
+
from crewai.tools.tool_failure import ToolFailure
|
|
7
|
+
|
|
8
|
+
from suno_crewai import SunoGenerateMusicTool, SunoRetrieveTaskTool
|
|
9
|
+
from suno_crewai._base import ServiceHTTPError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def reply(body, status=200):
|
|
13
|
+
return httpx.Response(
|
|
14
|
+
status,
|
|
15
|
+
json=body,
|
|
16
|
+
request=httpx.Request("POST", "https://api.acedata.cloud/suno/tasks"),
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def completed():
|
|
21
|
+
return {
|
|
22
|
+
"id": "existing",
|
|
23
|
+
"finished_at": 1,
|
|
24
|
+
"request": {"secret": "not-for-agent"},
|
|
25
|
+
"response": {
|
|
26
|
+
"success": True,
|
|
27
|
+
"data": [{"audio_url": "https://example.com/music.mp3"}],
|
|
28
|
+
"cost": {"amount": 0.1, "currency": "credit"},
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_submit_once_and_accepted_is_pending():
|
|
34
|
+
tool = SunoGenerateMusicTool(api_key="test-consumer-key")
|
|
35
|
+
with patch(
|
|
36
|
+
"httpx.Client.post", return_value=reply({"success": True, "task_id": "new"})
|
|
37
|
+
) as post:
|
|
38
|
+
result = json.loads(tool.run(prompt="piano", model="public-model"))
|
|
39
|
+
assert post.call_count == 1
|
|
40
|
+
assert post.call_args.args[0] == "https://api.acedata.cloud/suno/audios"
|
|
41
|
+
assert post.call_args.kwargs["json"] == {
|
|
42
|
+
"action": "generate",
|
|
43
|
+
"custom": False,
|
|
44
|
+
"prompt": "piano",
|
|
45
|
+
"model": "public-model",
|
|
46
|
+
"instrumental": True,
|
|
47
|
+
"async": True,
|
|
48
|
+
}
|
|
49
|
+
assert result == {
|
|
50
|
+
"status": "pending",
|
|
51
|
+
"success": False,
|
|
52
|
+
"task_id": "new",
|
|
53
|
+
"media_urls": [],
|
|
54
|
+
"cost": None,
|
|
55
|
+
}
|
|
56
|
+
assert isinstance(tool.run(prompt="again", model="public-model"), ToolFailure)
|
|
57
|
+
assert post.call_count == 1
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_read_same_id_and_filter_private_fields():
|
|
61
|
+
tool = SunoRetrieveTaskTool(api_key="test-consumer-key")
|
|
62
|
+
with patch("httpx.Client.post", return_value=reply(completed())) as post:
|
|
63
|
+
result = json.loads(tool.run(task_id="existing"))
|
|
64
|
+
assert post.call_args.kwargs["json"] == {"action": "retrieve", "id": "existing"}
|
|
65
|
+
assert result["status"] == "succeeded" and result["success"]
|
|
66
|
+
assert result["media_urls"] == ["https://example.com/music.mp3"]
|
|
67
|
+
assert "not-for-agent" not in str(result)
|
|
68
|
+
assert "test-consumer-key" not in str(tool.model_dump())
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_unfinished_preview_is_pending():
|
|
72
|
+
body = completed()
|
|
73
|
+
body["finished_at"] = None
|
|
74
|
+
with patch("httpx.Client.post", return_value=reply(body)):
|
|
75
|
+
result = json.loads(SunoRetrieveTaskTool(api_key="key").run(task_id="existing"))
|
|
76
|
+
assert result["status"] == "pending" and not result["media_urls"]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_terminal_failure_is_structured_and_not_retryable():
|
|
80
|
+
body = {
|
|
81
|
+
"id": "existing",
|
|
82
|
+
"finished_at": 1,
|
|
83
|
+
"response": {"success": False, "error": "fixture"},
|
|
84
|
+
}
|
|
85
|
+
with patch("httpx.Client.post", return_value=reply(body)):
|
|
86
|
+
result = SunoRetrieveTaskTool(api_key="key").run(task_id="existing")
|
|
87
|
+
assert isinstance(result, ToolFailure) and not result.retryable
|
|
88
|
+
assert result.code == "task_failed"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@pytest.mark.parametrize("status", [401, 403, 429, 500])
|
|
92
|
+
def test_http_status_retained_without_retry(status):
|
|
93
|
+
with patch(
|
|
94
|
+
"httpx.Client.post", return_value=reply({"error": "fixture"}, status)
|
|
95
|
+
) as post:
|
|
96
|
+
with pytest.raises(ServiceHTTPError) as caught:
|
|
97
|
+
SunoGenerateMusicTool(api_key="key").run(prompt="piano", model="model")
|
|
98
|
+
assert caught.value.status_code == status
|
|
99
|
+
assert post.call_count == 1
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_timeout_is_not_retried():
|
|
103
|
+
with patch(
|
|
104
|
+
"httpx.Client.post", side_effect=httpx.ReadTimeout("unknown submission outcome")
|
|
105
|
+
) as post:
|
|
106
|
+
with pytest.raises(TimeoutError):
|
|
107
|
+
SunoGenerateMusicTool(api_key="key").run(prompt="piano", model="model")
|
|
108
|
+
assert post.call_count == 1
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_bounded_wait_queries_only_same_task():
|
|
112
|
+
pending = {"id": "existing", "finished_at": None, "response": None}
|
|
113
|
+
with (
|
|
114
|
+
patch(
|
|
115
|
+
"httpx.Client.post", side_effect=[reply(pending), reply(completed())]
|
|
116
|
+
) as post,
|
|
117
|
+
patch("suno_crewai._base.time.monotonic", side_effect=[0, 1, 2]),
|
|
118
|
+
patch("suno_crewai._base.time.sleep"),
|
|
119
|
+
):
|
|
120
|
+
result = json.loads(
|
|
121
|
+
SunoRetrieveTaskTool(api_key="key").run(task_id="existing", wait_seconds=10)
|
|
122
|
+
)
|
|
123
|
+
assert result["success"] and post.call_count == 2
|
|
124
|
+
assert all(call.kwargs["json"]["id"] == "existing" for call in post.call_args_list)
|
|
125
|
+
assert all(call.args[0].endswith("/suno/tasks") for call in post.call_args_list)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_input_and_identity_checks():
|
|
129
|
+
with pytest.raises(ValueError):
|
|
130
|
+
SunoRetrieveTaskTool(api_key="key").run(task_id="existing", wait_seconds=241)
|
|
131
|
+
with patch("httpx.Client.post", return_value=reply(completed())):
|
|
132
|
+
with pytest.raises(ValueError, match="does not match"):
|
|
133
|
+
SunoRetrieveTaskTool(api_key="key").run(task_id="another")
|
|
134
|
+
with pytest.raises(ValueError, match="without Bearer"):
|
|
135
|
+
SunoRetrieveTaskTool(api_key="Bearer key")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_missing_key_fails_before_network(monkeypatch):
|
|
139
|
+
monkeypatch.delenv("ACEDATACLOUD_API_KEY", raising=False)
|
|
140
|
+
with pytest.raises(ValueError, match="ACEDATACLOUD_API_KEY"):
|
|
141
|
+
SunoRetrieveTaskTool()
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_missing_submission_id_is_unknown_not_success():
|
|
145
|
+
with patch("httpx.Client.post", return_value=reply({"success": True})):
|
|
146
|
+
with pytest.raises(ValueError, match="No task ID"):
|
|
147
|
+
SunoGenerateMusicTool(api_key="key").run(prompt="piano", model="model")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@pytest.mark.parametrize("where", ["top", "nested"])
|
|
151
|
+
def test_failed_state_before_timestamp_is_terminal(where):
|
|
152
|
+
body = completed()
|
|
153
|
+
body["finished_at"] = None
|
|
154
|
+
if where == "top":
|
|
155
|
+
body["state"] = "failed"
|
|
156
|
+
else:
|
|
157
|
+
body["response"]["data"][0]["status"] = "failed"
|
|
158
|
+
with patch("httpx.Client.post", return_value=reply(body)):
|
|
159
|
+
result = SunoRetrieveTaskTool(api_key="key").run(task_id="existing")
|
|
160
|
+
assert isinstance(result, ToolFailure) and not result.retryable
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_no_terminal_audio_is_not_success():
|
|
164
|
+
body = completed()
|
|
165
|
+
body["response"]["data"] = []
|
|
166
|
+
with patch("httpx.Client.post", return_value=reply(body)):
|
|
167
|
+
result = SunoRetrieveTaskTool(api_key="key").run(task_id="existing")
|
|
168
|
+
assert isinstance(result, ToolFailure) and result.code == "missing_terminal_media"
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_error_and_cost_payloads_do_not_leak_private_fields():
|
|
172
|
+
with patch(
|
|
173
|
+
"httpx.Client.post",
|
|
174
|
+
return_value=reply({"error": "private service diagnostic secret-fixture"}, 403),
|
|
175
|
+
):
|
|
176
|
+
with pytest.raises(ServiceHTTPError) as error:
|
|
177
|
+
SunoRetrieveTaskTool(api_key="secret-fixture").run(task_id="existing")
|
|
178
|
+
assert error.value.status_code == 403
|
|
179
|
+
assert "private service" not in str(error.value) and "secret-fixture" not in str(
|
|
180
|
+
error.value
|
|
181
|
+
)
|
|
182
|
+
body = completed()
|
|
183
|
+
body["response"]["cost"]["private_key"] = "private account data"
|
|
184
|
+
with patch("httpx.Client.post", return_value=reply(body)):
|
|
185
|
+
result = SunoRetrieveTaskTool(api_key="key").run(task_id="existing")
|
|
186
|
+
assert "private account data" not in result
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def test_management_token_is_rejected_before_network():
|
|
190
|
+
with pytest.raises(ValueError):
|
|
191
|
+
SunoRetrieveTaskTool(api_key="platform-management-fixture")
|