Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ dspy/

# Python
__pycache__/
.pytest_cache/
*.py[cod]
*$py.class
*.so
Expand Down
184 changes: 149 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,67 +3,181 @@
[![Documentation](https://img.shields.io/badge/docs-cmpnd--ai.github.io-blue)](https://cmpnd-ai.github.io/dspy-cli-tool/)
[![PyPI](https://img.shields.io/pypi/v/dspy-cli)](https://pypi.org/project/dspy-cli/)

CLI for deploying DSPy programs as HTTP APIs. Auto-generates endpoints, OpenAPI specs, and Docker configs.

Reduces deployment setup from hours to minutes for developers embedding LLM features in applications.
`dspy-cli` is a tool for creating, developing, testing, and deploying DSPy programs as HTTP APIs. `dspy-cli` Auto-generates endpoints, OpenAPI specs, and Docker configs.

## Quick Start

### Installing & Creating a New Project

To install `dspy-cli`, we recommend using `uv`.

```bash
# Install
uv tool install dspy-cli
```

Running `dspy-cli new` creates a new *project*, which can have many *programs*, each of which performs a specific task.

To illustrate, let's build a project that manages AI-powered functions we might want to call from a [content management system](https://en.wikipedia.org/wiki/Content_management_system) we use to draft, publish, and manage blog posts. We'll call it, "cms-kit".

```bash
dspy-cli new cms-kit
```

This command launches an interactive menu that walks you through setting up your first program and connecting to an inference provider.

Here's our answers:

```bash
Would you like to specify your first program? [Y/n]: Y
What is the name of your first DSPy program? [my_program]: summarizer
```

The first program we'll build will write short summaries of our blog posts. We'll call it `summarizer`.

Continuing:

```bash
Choose a module type:
1. Predict - Basic prediction module (default)
2. ChainOfThought (CoT) - Step-by-step reasoning with chain of thought
3. ProgramOfThought (PoT) - Generates and executes code for reasoning
4. ReAct - Reasoning and acting with tools
5. MultiChainComparison - Compare multiple reasoning paths
6. Refine - Iterative refinement of outputs
Enter number or name [1]: 1
Enter your signature or type '?' for guided input:
Examples: 'question -> answer', 'post:str -> tags:list[str], category:str'
Signature [question:str -> answer:str]: blog_post -> summary
```

Here we're using a basic `Predict` [module](https://dspy.ai/learn/programming/modules/) and specifying our [signature](https://dspy.ai/learn/programming/signatures/) as, `blog_post -> summary`.

Lastly, we'll connect to the model we want to use:

```bash
Enter your model (LiteLLM format):
Examples: 'anthropic/claude-sonnet-4-5', 'openai/gpt-4o', 'ollama/llama2'
Model [openai/gpt-5-mini]: openai/gpt-5-mini

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

syntax: openai/gpt-5-mini does not exist yet. Replace with an actual OpenAI model like openai/gpt-4o-mini

Suggested change
Model [openai/gpt-5-mini]: openai/gpt-5-mini
Model [openai/gpt-4o-mini]: openai/gpt-4o-mini
Prompt To Fix With AI
This is a comment left during a code review.
Path: README.md
Line: 60:60

Comment:
**syntax:** `openai/gpt-5-mini` does not exist yet. Replace with an actual OpenAI model like `openai/gpt-4o-mini`

```suggestion
Model [openai/gpt-4o-mini]: openai/gpt-4o-mini
```

How can I resolve this? If you propose a fix, please make it concise.

Enter your OpenAI API key:
(This will be stored in .env as OPENAI_API_KEY)
Press Enter to skip and set it manually later
OPENAI_API_KEY: your_key_here
```

# Create project
dspy-cli new blog-tagger -s "post -> tags: list[str]"
cd blog-tagger
DSPy uses [LiteLLM](https://www.litellm.ai/) to connect to language models, so we're using a [LiteLLM style string](https://docs.litellm.ai/docs/providers) to call GPT-5-mini. Paste in your OpenAI key, and you're good to go. `dspy-cli` will attempt to detect any API key variables in your local environment and will pre-populate this field if a candidate is found.

# Serve locally
`dspy-cli` will now create your project structure and define your first program. Let's `cd` into the folder `cms-kit`, activate our environment, and serve our project.

```bash
cd cms-kit
uv sync
source .venv/bin/activate
dspy-cli serve
```

Test the endpoint:
`dspy-cli` will detect the module we've defined, define an endpoint for it, then stand up an HTTP server to call, at `http://localhost:8000` (by default). Visiting that url will let you submit a form to call your program. Or, you can call the API endpoint directly, like so:

```bash
curl -X POST http://localhost:8000/BlogTaggerPredict \
curl -X POST http://0.0.0.0:8000/SummarizerPredict \
-H "Content-Type: application/json" \
-d '{"post": "How to build Chrome extensions with AI..."}'
-d '{
"blog_post": "[AN EXAMPLE BLOG POST]"
}'
```

Response:
### Creating Another Program

In addition to summarizing blogposts, we can imagine several other LLM-powered functions we could perform in our CMS: tagging, image description writing, drafting social media posts, etc.

```json
{
"tags": ["chrome-extensions", "ai", "development", "javascript"]
}
To add a new program to `cms_kit`, we can run the `generate scaffold` command:

```bash
dspy-cli generate scaffold tagger -s "blog_post -> tags:list[str]"
```

## Features
We name our program `tagger` and use the `-s` or `--signature` flag to pass in a signature detailing passing in a blog post and getting back a list of tags, which we specify as `list[str]`.

- Auto-discovery of modules as HTTP endpoints
- Docker configs and OpenAPI specs generated
- Hot reload development server
- Model switching via config file
- MCP tool support
`generate scaffold` creates our program by creating a *signature* and *module* file in `src/cms_kit/signatures` and `src/cms_kit/modules`, respectively.

## Commands
If we run `dspy-cli serve`, the new module will be discovered and hosted in the web UI and as a new API route.

```bash
dspy-cli new <name> [-s "input -> output"] # Create project
dspy-cli serve [--ui] # Start HTTP server
dspy-cli g scaffold <program> [-m CoT] # Add module to project
### Exploring Our Project

Running `dspy-cli new` sets up your project directory. Let's walk through a few key items created:

- `src/cms_kit/signatures`: [Class-based DSPy signatures](https://dspy.ai/learn/programming/signatures/#class-based-dspy-signatures) are created and housed here.
- `src/cms_kit/modules`: [DSPy modules](https://dspy.ai/learn/programming/modules/) are stored here. This folder is where `dspy-cli` discovers available programs when running `serve`.
- `src/cms_kit/utils`: While you can add arbitrary code to your module and signature files, `utils` is a handy place to stash additional logic and tool definitions. Just create, code, and import from your signatures and modules.
- `logs`: When your programs are called (via the web UI or API calls), usage is logged to a program-specific JSONL file, in the `logs` folder.
- `dspy.config.yaml`: This config file sets a few parameters, but is mainly where inference providers and models are defined, both globally and (if you want) on a per-program basis.

Speaking of model definitions...

### Connecting to Models

In `dspy.config.yaml` you're able to define language models you intend to call. We define models in a YAML format, with similar parameters to the LiteLLM integration [DSPy uses](https://dspy.ai/learn/programming/language_models/).

Here's what our model registry looks like in our `cms-kit` project:

```yaml
models:
# The default model to use if no per-program override is specified
default: openai:gpt-5-mini

# Model registry - define all available models here
registry:
openai:gpt-5-mini:
model: openai/gpt-5-mini
model_type: chat
max_tokens: 16000
temperature: 1.0
env: OPENAI_API_KEY
Comment on lines +125 to +134

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

syntax: openai:gpt-5-mini references a non-existent model. Update to use openai/gpt-4o-mini or another actual model

Suggested change
default: openai:gpt-5-mini
# Model registry - define all available models here
registry:
openai:gpt-5-mini:
model: openai/gpt-5-mini
model_type: chat
max_tokens: 16000
temperature: 1.0
env: OPENAI_API_KEY
# The default model to use if no per-program override is specified
default: openai:gpt-4o-mini
# Model registry - define all available models here
registry:
openai:gpt-4o-mini:
model: openai/gpt-4o-mini
model_type: chat
max_tokens: 16000
temperature: 1.0
env: OPENAI_API_KEY
Prompt To Fix With AI
This is a comment left during a code review.
Path: README.md
Line: 125:134

Comment:
**syntax:** `openai:gpt-5-mini` references a non-existent model. Update to use `openai/gpt-4o-mini` or another actual model

```suggestion
  # The default model to use if no per-program override is specified
  default: openai:gpt-4o-mini

  # Model registry - define all available models here
  registry:
    openai:gpt-4o-mini:
      model: openai/gpt-4o-mini
      model_type: chat
      max_tokens: 16000
      temperature: 1.0
      env: OPENAI_API_KEY
```

How can I resolve this? If you propose a fix, please make it concise.

```

In the `registry` list we define models, using the LiteLLM convention, like: "openai/gpt-5-mini". Here's what [Sonnet 4.5](https://www.anthropic.com/claude/sonnet) looks like:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

syntax: Reference to fictional model in documentation text. Replace with actual model name

Prompt To Fix With AI
This is a comment left during a code review.
Path: README.md
Line: 137:137

Comment:
**syntax:** Reference to fictional model in documentation text. Replace with actual model name

How can I resolve this? If you propose a fix, please make it concise.


```yaml
anthropic:sonnet-4.5:
model: anthropic/claude-sonnet-4-5
env: ANTHROPIC_API_KEY
max_tokens: 8192
temperature: 0.7
model_type: chat
```

Or Qwen3-4b hosted locally with [LM Studio](https://lmstudio.ai):

```yaml
qwen:qwen-3-4b:
model: openai/qwen/qwen3-4b
api_base: http://127.0.0.1:1234/v1
api_key: placeholder
max_tokens: 4096
temperature: 1.0
model_type: chat
```

The `default` key in the `models` list specifies the default model your programs will call. If you'd like, you can assign different programs to different models, like so:

```yaml
program_models:
TaggerPredict: qwen:qwen-3-4b
SummarizerPredict: anthropic:sonnet-4.5
```

See [Command Reference](docs/commands/) for complete documentation.
### Learning More

## Documentation
We've built out a lot of quality of life features in `dspy-cli`, including:

See the full docs at: [https://cmpnd-ai.github.io/dspy-cli-tool/](https://cmpnd-ai.github.io/dspy-cli-tool/)
- **Auto-discovery of modules:** Create a module in the `modules` folder and `dspy-cli` will detect the program and infer its parameters.
- **Type validation:** Typed parameters and return values defined in a module's `forward` method are validated during API calls.
- **Hot-reloading:** Tweaking signature or module definitions will cause `dspy-cli` to reload the server, updating the programs.
- **OpenAPI spec generation:** With each run of `serve`, `dspy-cli` creates an OpenAPI JSON definition of your API, which is accessible at `/openapi.json`.
- **MCP tool support:** Pass in `--mcp` while calling `serve` to stand up an MCP server for your program.
- **Docker configuration:** Running `new` creates a Dockerfile, which can be used to quickly stand up your program in Docker.

- [Getting Started](docs/getting-started.md) - Quickstart guide
- [Commands](docs/commands/) - CLI reference
- [Configuration](docs/configuration.md) - Model and environment settings
- [Examples](examples/) - Sample projects
Check out [the full docs](https://cmpnd-ai.github.io/dspy-cli-tool/) to learn more.

## License
### License

MIT
55 changes: 54 additions & 1 deletion src/dspy_cli/utils/model_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,51 @@ def is_local_model(provider: str) -> bool:
return provider.lower() in local_providers


def is_reasoning_model(provider: str, model: str) -> bool:
"""Check if a model is an OpenAI reasoning model that requires higher max_tokens.

OpenAI reasoning models (o1, o3, gpt-5 series) require higher max_tokens limits
to accommodate their extended reasoning processes.

Args:
provider: Provider name (e.g., "openai")
model: Model name (e.g., "o1-preview", "gpt-5-mini", "gpt-5.1")

Returns:
True if model is a reasoning model that requires max_tokens=16000

Examples:
>>> is_reasoning_model("openai", "o1-preview")
True

>>> is_reasoning_model("openai", "gpt-5-mini")
True

>>> is_reasoning_model("openai", "gpt-4o")
False
"""
if provider.lower() != 'openai':
return False

# Check for o1-* and o3-* series
if model.startswith('o1-') or model.startswith('o3-'):
return True

# Check for gpt-5 (exact match)
if model == 'gpt-5':
return True

# Check for gpt-5-* series (e.g., gpt-5-mini)
if model.startswith('gpt-5-'):
return True

# Check for gpt-5.x versions (e.g., gpt-5.1, gpt-5.2)
if model.startswith('gpt-5.'):
return True

return False


def detect_api_key(provider: str) -> tuple[str | None, str]:
"""Detect API key environment variable for a given provider.

Expand Down Expand Up @@ -111,12 +156,20 @@ def generate_model_config(model_str: str, api_key: str | None, api_base: str | N
"""
parsed = parse_model_string(model_str)
provider = parsed['provider']
model = parsed['model']
_, env_var_name = detect_api_key(provider)

# Determine max_tokens based on model type
# OpenAI reasoning models (o1/o3/gpt-5) need higher limits for extended reasoning
if is_reasoning_model(provider, model):
max_tokens = 16000
else:
max_tokens = 8192 # Increased default from 4096

config = {
'model': model_str,
'model_type': 'chat',
'max_tokens': 4096,
'max_tokens': max_tokens,
'temperature': 1.0,
}

Expand Down
49 changes: 49 additions & 0 deletions tests/test_commands_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,3 +189,52 @@ def test_generate_outside_project(runner, tmp_cwd):
res = runner.invoke(main, ["g", "scaffold", "test"], catch_exceptions=False)
assert res.exit_code != 0
assert "Not in a valid DSPy project" in res.output


def test_new_with_reasoning_model(runner, tmp_cwd):
"""Test 'new' command with OpenAI reasoning models sets max_tokens=16000."""
test_cases = [
("openai/o1-preview", 16000),
("openai/o1-mini", 16000),
("openai/o3-mini", 16000),
("openai/gpt-5", 16000),
("openai/gpt-5-mini", 16000),
("openai/gpt-5.1", 16000),
]

for model, expected_max_tokens in test_cases:
project_name = f"test-{model.replace('/', '-').replace('.', '-')}"
res = runner.invoke(
main,
with_new_defaults(["new", project_name, "--model", model]),
catch_exceptions=False
)
assert res.exit_code == 0

proj = tmp_cwd / project_name
config_content = (proj / "dspy.config.yaml").read_text()
assert f"max_tokens: {expected_max_tokens}" in config_content, \
f"Expected max_tokens: {expected_max_tokens} for model {model}"


def test_new_with_standard_model_uses_new_default(runner, tmp_cwd):
"""Test 'new' command with standard models uses increased default max_tokens=8192."""
test_cases = [
"openai/gpt-4o",
"openai/gpt-4o-mini",
"anthropic/claude-sonnet-4-5",
]

for model in test_cases:
project_name = f"test-{model.replace('/', '-')}"
res = runner.invoke(
main,
with_new_defaults(["new", project_name, "--model", model]),
catch_exceptions=False
)
assert res.exit_code == 0

proj = tmp_cwd / project_name
config_content = (proj / "dspy.config.yaml").read_text()
assert "max_tokens: 8192" in config_content, \
f"Expected max_tokens: 8192 for standard model {model}"