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.
@@ -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,9 @@
1
+ .venv/
2
+ __pycache__/
3
+ .pytest_cache/
4
+ .ruff_cache/
5
+ dist/
6
+ *.egg-info/
7
+ .env
8
+ .env.*
9
+ !/.env.example
@@ -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
+ ![Copy API Key or open Manage Keys; credentials redacted](https://raw.githubusercontent.com/AceDataCloud/SunoCrewAI/v0.1.0-preview.1/assets/tutorial/get-api-key-en.png)
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
+ ![Create optional dedicated API Key; historical screenshot](https://raw.githubusercontent.com/AceDataCloud/SunoCrewAI/v0.1.0-preview.1/assets/tutorial/create-api-key-en.png)
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
+ ![Official historical CrewAI execution trace, not Ace runtime evidence](https://raw.githubusercontent.com/AceDataCloud/SunoCrewAI/v0.1.0-preview.1/assets/tutorial/official-crewai-tracing.png)
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
+ ![Copy API Key or open Manage Keys; credentials redacted](assets/tutorial/get-api-key-en.png)
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
+ ![Create optional dedicated API Key; historical screenshot](assets/tutorial/create-api-key-en.png)
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
+ ![Official historical CrewAI execution trace, not Ace runtime evidence](assets/tutorial/official-crewai-tracing.png)
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
+ ![Copy API Key or open Manage Keys; credentials redacted](https://raw.githubusercontent.com/AceDataCloud/SunoCrewAI/v0.1.0-preview.1/assets/tutorial/get-api-key-en.png)
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
+ ![Create optional dedicated API Key; historical screenshot](https://raw.githubusercontent.com/AceDataCloud/SunoCrewAI/v0.1.0-preview.1/assets/tutorial/create-api-key-en.png)
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
+ ![Official historical CrewAI execution trace, not Ace runtime evidence](https://raw.githubusercontent.com/AceDataCloud/SunoCrewAI/v0.1.0-preview.1/assets/tutorial/official-crewai-tracing.png)
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
@@ -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
+ ![复制 API Key 或进入 Manage Keys,凭据均已遮挡](../assets/tutorial/get-api-key-en.png)
28
+
29
+ 4. 将新 Key 命名为 `CrewAI Suno`,按需配置过期、额度和权限。若启用 **Allowed APIs**,同时允许 `/suno/audios` 与 `/suno/tasks`。
30
+
31
+ ![创建专用 Key 的历史真实截图](../assets/tutorial/create-api-key-en.png)
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
+ ![CrewAI 官方历史执行详情,仅作交互参考,非 Ace 跑通](../assets/tutorial/official-crewai-tracing.png)
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,5 @@
1
+ """Suno by Ace Data Cloud for CrewAI. Not affiliated with Suno or CrewAI."""
2
+
3
+ from suno_crewai.tools import SunoGenerateMusicTool, SunoRetrieveTaskTool
4
+
5
+ __all__ = ["SunoGenerateMusicTool", "SunoRetrieveTaskTool"]
@@ -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")