# Dedalus Machines API Source: https://docs.dedaluslabs.ai/api-reference/dcs Raw HTTP contracts for Dedalus Machines. Use this section when you need the exact request shape, response schema, or interactive playground for the Dedalus Machines API. For workflow examples, use the product guides. ## Wire contract | Field | Value | | ----------- | --------------------------------------------------------------------------------------------- | | Base URL | `https://dcs.dedaluslabs.ai` | | Auth | `Authorization: Bearer $DEDALUS_API_KEY` | | Idempotency | Send `Idempotency-Key` on mutating requests. Reuse a key only when retrying the same request. | ## Workflow guides List, update, sleep, wake, watch, and delete machines. Run non-interactive commands and poll for output. Open short-lived SSH access with strict host checking. Stream interactive PTY sessions over WebSocket. Endpoint pages in the sidebar are generated from the DCS OpenAPI spec. They are the source of truth for parameters, request bodies, response schemas, and playground requests. # Agents API Source: https://docs.dedaluslabs.ai/api/index One endpoint. Any model. OpenAI-compatible. Dedalus Agents API is an OpenAI-compatible endpoint that routes to every major LLM provider. One API key. Any model from Anthropic, OpenAI, Google, Meta, xAI, DeepSeek, Mistral, and more. ## What you get * **One key, any model.** Switch providers with a string change. * **OpenAI-compatible.** Works with the OpenAI SDK, OpenAI Agents SDK, Vercel AI SDK, or any OpenAI-compatible client. * **Streaming, tools, structured outputs, multimodal.** All standard fields pass through. * **Automatic retries and fallbacks** across providers. * **Token-for-token pricing.** No markup. [BYOK](/sdk/agents/byok) supported. ## Supported models Every major provider, routed through one endpoint. See the full catalog in [Supported Models](/api/models), or fetch the live list via [`GET /v1/models`](/api/list-models). Latest additions: `anthropic/claude-opus-4-6`, `openai/gpt-5.2`, `google/gemini-3-pro-preview`, `xai/grok-4-1-fast-reasoning`. ## Quickstart Use the official Dedalus SDK: ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio from dedalus_labs import AsyncDedalus, DedalusRunner async def main(): client = AsyncDedalus() runner = DedalusRunner(client) response = await runner.run( input="What is the capital of France?", model="anthropic/claude-opus-4-5", ) print(response.final_output) asyncio.run(main()) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus, { DedalusRunner } from "dedalus-labs"; const client = new Dedalus(); const runner = new DedalusRunner(client); const response = await runner.run({ input: "What is the capital of France?", model: "anthropic/claude-opus-4-5", }); console.log(response.finalOutput); ``` ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}} curl -X POST https://api.dedaluslabs.ai/v1/chat/completions \ -H "Authorization: Bearer $DEDALUS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "anthropic/claude-opus-4-5", "messages": [{"role": "user", "content": "What is the capital of France?"}] }' ``` **OpenAI-compatible.** The same endpoint works drop-in with the OpenAI SDK, the OpenAI Agents SDK, the Vercel AI SDK, or any OpenAI-compatible client. Point `baseURL` at `https://api.dedaluslabs.ai/v1` and pass your Dedalus API key. ## Also works with Set `baseURL` and your Dedalus key. Full agent loop, compatible out of the box. Use the openai-compatible provider. ## Capabilities All standard OpenAI Chat Completions fields pass through. Deeper guides live in the SDK section: Server-sent events, token-by-token. Let the model call your functions. Pydantic and Zod schemas, enforced. Browse the full catalog. ## API Reference Full endpoint-level reference lives in [API Reference > Agents API](/api-reference/api). # Supported Models Source: https://docs.dedaluslabs.ai/api/models Every model from every major provider, reachable through one OpenAI-compatible endpoint. Dedalus routes to every major LLM provider. Models are addressed with the format `provider/model-name`, and you switch providers by changing a string. **Programmatic discovery:** fetch the live list with [`GET /v1/models`](/api/list-models). Each model object includes `capabilities` (vision, tools, thinking, streaming) and `provider_info` (status, upstream API). ## Providers `openai/gpt-5.2` `anthropic/claude-opus-4-6` `google/gemini-3-pro-preview` `xai/grok-4-1-fast-reasoning` `deepseek/deepseek-reasoner` `mistral/mistral-large-latest` `groq/llama-3.3-70b-versatile` `cerebras/llama-3.3-70b` `moonshot/kimi-k2.5` `perplexity/sonar-pro` `fireworks/llama-v3p1-405b` `cohere/command-r-plus` ## Model catalog ### OpenAI #### Chat * `openai/gpt-5.2` * `openai/gpt-5.1` * `openai/gpt-5` * `openai/gpt-5-mini` * `openai/gpt-5-nano` * `openai/gpt-5-chat-latest` * `openai/gpt-4.1` * `openai/gpt-4.1-mini` * `openai/gpt-4.1-nano` * `openai/gpt-4o` * `openai/gpt-4o-2024-05-13` * `openai/chatgpt-4o-latest` * `openai/gpt-4-turbo` * `openai/gpt-4` * `openai/gpt-3.5-turbo` #### Reasoning * `openai/o1` * `openai/o3` * `openai/o3-mini` * `openai/o4-mini` #### Image generation * `openai/dall-e-3` #### Audio transcription * `openai/whisper-1` #### Embeddings | Model | Price | | ------------------------------- | ------------------ | | `openai/text-embedding-3-large` | \$0.13 / 1M tokens | | `openai/text-embedding-3-small` | \$0.02 / 1M tokens | | `openai/text-embedding-ada-002` | \$0.10 / 1M tokens | ### Anthropic #### Claude 4.6 * `anthropic/claude-opus-4-6` #### Claude 4.5 * `anthropic/claude-opus-4-5` * `anthropic/claude-sonnet-4-5-20250929` * `anthropic/claude-haiku-4-5-20251001` #### Claude 4 * `anthropic/claude-opus-4-1-20250805` * `anthropic/claude-opus-4-20250514` #### Claude 3.7 * `anthropic/claude-3-7-sonnet-20250219` #### Claude 3.5 * `anthropic/claude-3-5-haiku-20241022` #### Claude 3 * `anthropic/claude-3-haiku-20240307` ### Google #### Gemini 3 * `google/gemini-3-pro-preview` * `google/gemini-3-flash-preview` #### Gemini 2.5 * `google/gemini-2.5-pro` * `google/gemini-2.5-flash` * `google/gemini-2.5-flash-lite` #### Gemini 2.0 * `google/gemini-2.0-flash` * `google/gemini-2.0-flash-exp` * `google/gemini-2.0-flash-001` * `google/gemini-2.0-flash-lite` #### Embeddings * `google/text-embedding-004` ### xAI #### Grok 4 * `xai/grok-4-1-fast-reasoning` * `xai/grok-4-1-fast-non-reasoning` * `xai/grok-4-fast-reasoning` * `xai/grok-4-fast-non-reasoning` * `xai/grok-code-fast-1` * `xai/grok-4-0709` #### Grok 3 * `xai/grok-3` * `xai/grok-3-mini` #### Grok 2 * `xai/grok-2-vision-1212` ### DeepSeek * `deepseek/deepseek-chat` * `deepseek/deepseek-reasoner` * `deepseek/deepseek-coder` ### Mistral * `mistral/mistral-large-latest` * `mistral/mistral-medium-latest` * `mistral/mistral-small-latest` * `mistral/codestral-2508` * `mistral/open-mistral-nemo-2407` * `mistral/pixtral-12b` ### Groq Lightning-fast inference for open-source models. * `groq/llama-3.1-8b-instant` * `groq/llama-3.3-70b-versatile` * `groq/openai/gpt-oss-120b` * `groq/openai/gpt-oss-20b` * `groq/whisper-large-v3` * `groq/whisper-large-v3-turbo` ### Cerebras Ultra-fast inference on custom silicon. #### Production * `cerebras/llama3.1-8b` * `cerebras/llama-3.3-70b` * `cerebras/gpt-oss-120b` * `cerebras/qwen-3-32b` #### Preview * `cerebras/qwen-3-235b-a22b-instruct-2507` * `cerebras/zai-glm-4.7` ### Moonshot * `moonshot/kimi-k2.5` * `moonshot/kimi-k2-0905-preview` * `moonshot/kimi-k2-0711-preview` * `moonshot/kimi-k2-turbo-preview` * `moonshot/kimi-k2-thinking` * `moonshot/kimi-k2-thinking-turbo` ## Recommendations by use case ### Tool calling and function use * `anthropic/claude-opus-4-6` - strongest tool calling with structured outputs * `anthropic/claude-sonnet-4-5-20250929` - fast, reliable tool use * `openai/gpt-5.2` - native function calling with structured responses * `openai/gpt-4o` - reliable for production tool workflows * `deepseek/deepseek-chat` - multi-step reasoning with tools ### Coding * `openai/gpt-5-codex` - purpose-built for code generation * `deepseek/deepseek-coder` - strong code-focused model * `anthropic/claude-opus-4-6` - excellent code understanding * `xai/grok-code-fast-1` - fast code-focused inference * `mistral/codestral-2508` - open-source coding model ### Reasoning * `openai/o3` - deep reasoning for complex problems * `openai/o1` - multi-step chain-of-thought * `anthropic/claude-opus-4-6` - advanced reasoning capabilities * `deepseek/deepseek-reasoner` - specialized reasoning model * `xai/grok-4-1-fast-reasoning` - optimized for reasoning tasks ### Speed and throughput * `anthropic/claude-haiku-4-5-20251001` - fast Claude at lower cost * `google/gemini-2.5-flash` - optimized for throughput * `openai/gpt-5-mini` - lightweight, fast * `openai/gpt-5-nano` - ultra-fast for simple tasks * `groq/llama-3.3-70b-versatile` - Groq-accelerated open-source ### Long context * `google/gemini-3-pro-preview` - 1M+ token context * `google/gemini-2.5-pro` - extended context with strong reasoning * `anthropic/claude-opus-4-6` - long-context analysis * `anthropic/claude-sonnet-4-5-20250929` - fast long-context ### Vision and multimodal * `openai/gpt-5.2` - strong multimodal capabilities * `anthropic/claude-opus-4-6` - advanced vision understanding * `google/gemini-3-pro-preview` - vision plus long context * `xai/grok-2-vision-1212` - multimodal with Grok * `openai/gpt-4o` - reliable vision for production Most providers ship multiple tiers (mini / standard / pro / opus). Start on the smaller tier, scale up only when the benchmarks justify it. # Create machine Source: https://docs.dedaluslabs.ai/dcs/api/machine-lifecycle/create-machine /dcs-openapi.json post /v1/machines # Get machine Source: https://docs.dedaluslabs.ai/dcs/api/machine-lifecycle/get-machine /dcs-openapi.json get /v1/machines/{machine_id} # List machines Source: https://docs.dedaluslabs.ai/dcs/api/machine-lifecycle/list-machines /dcs-openapi.json get /v1/machines # Update machine Source: https://docs.dedaluslabs.ai/dcs/api/machine-lifecycle/update-machine /dcs-openapi.json patch /v1/machines/{machine_id} # CLI Source: https://docs.dedaluslabs.ai/dcs/cli Install, update, and use the Dedalus CLI for Cloud Services. The Dedalus CLI manages Dedalus Cloud Services (DCS) from a terminal or automated workflow. API commands group actions under resources such as `machines` and `usage`. This guide covers stable workflows and command families. For the exact commands and flags supported by your installed version, use the generated `--help` output. ## Install ```bash Homebrew theme={"theme":{"light":"github-light","dark":"github-dark"}} brew install dedalus-labs/tap/dedalus ``` ```bash Go theme={"theme":{"light":"github-light","dark":"github-dark"}} go install github.com/dedalus-labs/dedalus-cli/cmd/dedalus@latest ``` ```bash curl theme={"theme":{"light":"github-light","dark":"github-dark"}} curl -fsSL https://www.dedaluslabs.ai/install/dedalus | bash ``` ```powershell PowerShell theme={"theme":{"light":"github-light","dark":"github-dark"}} irm https://raw.githubusercontent.com/dedalus-labs/dedalus-cli/main/scripts/install.ps1 | iex ``` Homebrew installs the version currently published in the tap. Compare `dedalus --version` with the [latest CLI release](https://github.com/dedalus-labs/dedalus-cli/releases/latest); if the tap trails the release, use Go, curl, or PowerShell. Verify the installation and inspect the available commands: ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} dedalus --version dedalus --help ``` ## Authenticate Create an API key in the [Dedalus Dashboard](https://dedaluslabs.ai/dashboard/api-keys), then export it in your shell: ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} export DEDALUS_API_KEY="your-api-key" ``` Prefer the environment variable to `--api-key` in scripts. This keeps the key out of shell history and command arguments. Verify that the CLI can access your account: ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} dedalus machines list ``` ## Update Update the current installation, or check for a newer release without installing it: ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} dedalus update dedalus update --check ``` Homebrew and curl-installer installations update in place. On Windows, the command prints the PowerShell update command. Go users should rerun `go install github.com/dedalus-labs/dedalus-cli/cmd/dedalus@latest`; other unrecognized installs receive manual instructions. During interactive use, the CLI periodically checks for a newer release and asks before updating. Continuous integration jobs and commands without an interactive terminal skip this check. Set `DEDALUS_NO_UPDATE_CHECK=1` to disable it in interactive shells. If an older CLI does not recognize `dedalus update`, reinstall it with the Go, curl, or PowerShell command above. ## Command structure API commands follow the resource hierarchy: ```text theme={"theme":{"light":"github-light","dark":"github-dark"}} dedalus [flags] dedalus machines [flags] ``` | Command family | Use it to | | -------------------------------- | --------------------------------------------------------------------- | | `dedalus machines` | Create, inspect, update, delete, sleep, wake, and watch Machines. | | `dedalus machines ` | Manage artifacts, executions, previews, SSH sessions, and terminals. | | `dedalus usage` | Inspect organization totals and per-Machine compute or storage usage. | | `dedalus ssh` | Open an SSH session to a running Machine. | | `dedalus update` | Check for or install a newer CLI release. | ### Manage a Machine ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} dedalus machines create \ --vcpu 1 \ --memory-mib 2048 \ --storage-gib 10 \ --autosleep 15m dedalus machines list dedalus machines retrieve --machine-id dm-abc123 dedalus machines update --machine-id dm-abc123 --autosleep never dedalus machines watch --machine-id dm-abc123 ``` See [Machine lifecycle](/dcs/dm/lifecycle) for the state model and operations. ### Work with Machine resources ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} dedalus machines executions create \ --machine-id dm-abc123 \ --command '["echo", "hello"]' dedalus machines artifacts list --machine-id dm-abc123 dedalus machines terminals list --machine-id dm-abc123 dedalus machines previews list --machine-id dm-abc123 dedalus ssh dm-abc123 ``` For workflow details, see [Executions](/dcs/dm/executions), [Terminals](/dcs/dm/terminals), [Ports](/dcs/dm/ports), and [SSH](/dcs/dm/ssh). ### Inspect organization usage Usage commands use the organization associated with your API key: ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} dedalus usage retrieve dedalus usage machine-compute --granularity day dedalus usage machine-storage --machine-id dm-abc123 ``` ## Control input and output The default output format is JSON. Use `--format` for human-readable YAML or the interactive explorer: ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} dedalus machines list --format yaml dedalus machines list --format explore ``` GJSON is the field-selection syntax for CLI JSON output. Use [GJSON path syntax](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) to transform each list item. This example prints one Machine ID per line without JSON quotes: ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} dedalus machines list --transform machine_id --raw-output ``` Commands with a request body also accept JSON or YAML from standard input: ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} dedalus machines create <<'YAML' vcpu: 2 memory_mib: 4096 storage_gib: 20 autosleep: 15m YAML ``` Run `dedalus --help` to inspect pagination flags and accepted input fields. Run `dedalus --help` to see the global output formats for your version. ## Shell completions Homebrew installs completions automatically. Manual installations can generate a script for Bash, `zsh`, Fish, or PowerShell. The command writes the script to standard output; source it for the current session or save it where your shell loads completions. ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} dedalus @completion bash > dedalus.bash dedalus @completion zsh > _dedalus dedalus @completion fish > dedalus.fish dedalus @completion pwsh > dedalus.ps1 ``` ## Command reference The CLI generates its reference from the same command tree it executes. Add `--help` at any level to see the exact flags and nested commands in the installed release: ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} dedalus --help dedalus machines --help dedalus machines executions create --help ``` See the [CLI repository](https://github.com/dedalus-labs/dedalus-cli) for release notes and source code. # What are Dedalus Machines? Source: https://docs.dedaluslabs.ai/dcs/dm/dedalus-machines DCS Linux VMs for agents and software workloads. Dedalus Machines is an offering of Dedalus Cloud Services (DCS). A Dedalus Machine is a Linux VM that wakes in under 50ms and keeps its filesystem across sleep. Each machine has its own Linux kernel and filesystem. It runs systemd, supports SSH, and runs commands through the API. Dedalus Machines are in public beta. ## Model Every machine has a `machine_id`. Use it to retrieve state, run commands, open SSH, watch lifecycle, sleep, wake, or delete. Set vCPU, memory, storage, and autosleep at create or update. Plan limits apply. | Surface | Use | Status | | ---------- | ----------------------------------------------------------------- | ------------------ | | Lifecycle | Create, inspect, update, sleep, wake, watch, and delete machines. | Public beta | | Executions | Run commands without opening SSH. | Public beta | | SSH | Open an interactive OpenSSH session. | Public beta | | Terminals | Open an interactive terminal over WebSocket. | Public beta | | Artifacts | Fetch files captured by executions. | Public beta | | Ports | Route traffic to a port inside the VM. | Not in public beta | | Usage | View billing usage. | Public beta | ## Lifecycle Machines stay awake as long as the workload needs. There is no fixed platform timeout. Autosleep is enabled by default with a 5-minute idle window. Set `autosleep` to `never` to disable automatic sleep. Sleep deallocates RAM and discards memory state. Compute billing stops. The filesystem persists. Wake starts from the persisted filesystem. Dedalus moves machines between hosts during failures or maintenance. ## Filesystem The filesystem is the durable state of a machine. Files written under paths like `/home`, `/root`, `/etc`, `/usr/local`, and `/var` survive sleep and wake. Runtime filesystems such as `/proc`, `/sys`, `/dev`, `/run`, and `/dev/shm` are recreated on each wake. Treat `/tmp` as temporary scratch space. Use `/var/tmp`, `/home`, `/root`, or another root-filesystem path for files you need later. Storage expansion depends on your plan. ## Isolation Dedalus Machines use VM-level isolation. Each machine gets its own Linux kernel. Agents run untrusted code, install dependencies, compile projects, and execute shell commands. VM isolation gives each workload a hardware-backed boundary. ## Tools for agents Use the CLI, Python SDK, TypeScript SDK, Go SDK, or HTTP API from agents and backends. For tool calls, use executions. Send a command and `machine_id`. The API returns an `execution_id`. Use it to retrieve status, events, stdout, stderr, and output. ## Common use cases Use a machine when an agent needs a real Linux environment for one tool call. Common workloads: builds, tests, CI runners, bursty backend jobs, long-running services, SSH debugging, Docker, and nested virtualization through `/dev/kvm`. ## Current limits Linux is the only supported operating system today. The base image is intentionally small. Outside public beta: custom images, machine forking, memory snapshots, general file upload and download APIs, port URLs, GPU machines, premium storage tiers, and self-service retention policies. Public beta limits vary by account and plan. See [Pricing](/dcs/dm/pricing) for current plan limits. ## Delete Delete removes a machine from normal use immediately. Storage may be retained for up to 30 days. Recovery requires support. Contact support immediately after mistaken deletion. ## Read next Start with the [Quickstart](/dcs/dm/quickstart). Use [Lifecycle](/dcs/dm/lifecycle) for state, [Executions](/dcs/dm/executions) for commands, [SSH](/dcs/dm/ssh) for interactive access, and the [API reference](/api-reference/dcs) for the API contract. # Executions Source: https://docs.dedaluslabs.ai/dcs/dm/executions Run arbitrary shell commands on Dedalus Machines ## Run a command Use executions when an agent or backend needs to run a command without opening SSH. Execution is asynchronous: create returns immediately, you poll until terminal, then fetch output. ```bash CLI theme={"theme":{"light":"github-light","dark":"github-dark"}} dedalus machines executions create \ --machine-id "$MACHINE_ID" \ --command '["echo", "hello world"]' ``` ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} import time from dedalus_sdk import Dedalus TERMINAL = {"succeeded", "failed", "cancelled", "expired"} def run(client: Dedalus, machine_id: str, command: list[str], **kwargs) -> str: """Block until the execution finishes; return stdout. Raises on failure.""" exc = client.machines.executions.create(machine_id=machine_id, command=command, **kwargs) delay = 0.1 while exc.status not in TERMINAL: wait = (exc.retry_after_ms or 0) / 1000 if exc.status == "wake_in_progress" else delay time.sleep(wait) delay = min(delay * 2, 2.0) exc = client.machines.executions.retrieve(machine_id=machine_id, execution_id=exc.execution_id) if exc.status != "succeeded": raise RuntimeError(f"{exc.status}: {exc.error_code}: {exc.error_message}") out = client.machines.executions.output(machine_id=machine_id, execution_id=exc.execution_id) return out.stdout or "" print(run(client, machine_id, ["/bin/bash", "-c", "uname -a"])) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from "dedalus"; const TERMINAL = new Set(["succeeded", "failed", "cancelled", "expired"]); const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); async function run( client: Dedalus, machineId: string, command: string[], opts: { stdin?: string; env?: Record; cwd?: string; timeout_ms?: number } = {}, ): Promise { let exc = await client.machines.executions.create({ machine_id: machineId, command, ...opts, }); let delay = 100; while (!TERMINAL.has(exc.status)) { const wait = exc.status === "wake_in_progress" ? (exc.retry_after_ms ?? 0) : delay; await sleep(wait); delay = Math.min(delay * 2, 2000); exc = await client.machines.executions.retrieve({ machine_id: machineId, execution_id: exc.execution_id, }); } if (exc.status !== "succeeded") { throw new Error(`${exc.status}: ${exc.error_code}: ${exc.error_message}`); } const out = await client.machines.executions.output({ machine_id: machineId, execution_id: exc.execution_id, }); return out.stdout ?? ""; } console.log(await run(client, machineId, ["/bin/bash", "-c", "uname -a"])); ``` ```go Go theme={"theme":{"light":"github-light","dark":"github-dark"}} func run(ctx context.Context, client dedalus.Client, machineID string, command []string) (string, error) { done := map[dedalus.ExecutionStatus]bool{ dedalus.ExecutionStatusSucceeded: true, dedalus.ExecutionStatusFailed: true, dedalus.ExecutionStatusCancelled: true, dedalus.ExecutionStatusExpired: true, } exc, err := client.Machines.Executions.New(ctx, dedalus.MachineExecutionNewParams{ MachineID: machineID, ExecutionCreateParams: dedalus.ExecutionCreateParams{ Command: command, }, }) if err != nil { return "", err } for !done[exc.Status] { time.Sleep(500 * time.Millisecond) exc, err = client.Machines.Executions.Get(ctx, dedalus.MachineExecutionGetParams{ MachineID: machineID, ExecutionID: exc.ExecutionID, }) if err != nil { return "", err } } if exc.Status != dedalus.ExecutionStatusSucceeded { return "", fmt.Errorf("%s: %s", exc.ErrorCode, exc.ErrorMessage) } out, err := client.Machines.Executions.Output(ctx, dedalus.MachineExecutionOutputParams{ MachineID: machineID, ExecutionID: exc.ExecutionID, }) if err != nil { return "", err } return out.Stdout, nil } stdout, _ := run(ctx, client, machineID, []string{"/bin/bash", "-c", "uname -a"}) fmt.Println(stdout) ``` ## Create Start an execution. Returns immediately. ```bash CLI theme={"theme":{"light":"github-light","dark":"github-dark"}} dedalus machines executions create --machine-id "$MACHINE_ID" --command '["whoami"]' ``` ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} exc = client.machines.executions.create( machine_id=machine_id, command=["/bin/bash", "-c", "whoami"], ) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} const exc = await client.machines.executions.create({ machine_id: machineId, command: ["/bin/bash", "-c", "whoami"], }); ``` ```go Go theme={"theme":{"light":"github-light","dark":"github-dark"}} exc, _ := client.Machines.Executions.New(ctx, dedalus.MachineExecutionNewParams{ MachineID: machineID, ExecutionCreateParams: dedalus.ExecutionCreateParams{ Command: []string{"/bin/bash", "-c", "whoami"}, }, }) fmt.Println(exc.ExecutionID) ``` The machine to run the command on. `argv` array. The first element is the executable; the rest are arguments. Bytes to pipe to the process's stdin. Extra environment variables for this process. Working directory. Hard kill after this many milliseconds. ## Retrieve Get the current state of an execution. Use this to poll until terminal. ```bash CLI theme={"theme":{"light":"github-light","dark":"github-dark"}} dedalus machines executions retrieve --machine-id "$MACHINE_ID" --execution-id "$EXEC_ID" ``` ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} exc = client.machines.executions.retrieve(machine_id=machine_id, execution_id="") ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} const exc = await client.machines.executions.retrieve({ machine_id: machineId, execution_id: "", }); ``` ```go Go theme={"theme":{"light":"github-light","dark":"github-dark"}} exc, _ := client.Machines.Executions.Get(ctx, dedalus.MachineExecutionGetParams{ MachineID: machineID, ExecutionID: "", }) fmt.Println(exc.Status) ``` The machine that owns the execution. The execution to retrieve. ## List List executions on a machine, newest first. ```bash CLI theme={"theme":{"light":"github-light","dark":"github-dark"}} dedalus machines executions list --machine-id "$MACHINE_ID" ``` ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} page = client.machines.executions.list(machine_id=machine_id) for exc in page.items or []: print(exc.execution_id, exc.status) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} const page = await client.machines.executions.list({ machine_id: machineId }); for (const exc of page.items ?? []) console.log(exc.execution_id, exc.status); ``` ```go Go theme={"theme":{"light":"github-light","dark":"github-dark"}} page, _ := client.Machines.Executions.List(ctx, dedalus.MachineExecutionListParams{ MachineID: machineID, }) for _, exc := range page.Items { fmt.Println(exc.ExecutionID, exc.Status) } ``` The machine whose executions you want to list. Pagination cursor from a prior page's `next_cursor`. Max items per page. ## Delete Cancel a running execution. No-op if already terminated. ```bash CLI theme={"theme":{"light":"github-light","dark":"github-dark"}} dedalus machines executions delete --machine-id "$MACHINE_ID" --execution-id "$EXEC_ID" ``` ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} client.machines.executions.delete(machine_id=machine_id, execution_id="") ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} await client.machines.executions.delete({ machine_id: machineId, execution_id: "", }); ``` ```go Go theme={"theme":{"light":"github-light","dark":"github-dark"}} client.Machines.Executions.Delete(ctx, dedalus.MachineExecutionDeleteParams{ MachineID: machineID, ExecutionID: "", }) ``` The machine that owns the execution. The execution to cancel. ## Events Per-execution event log: stdout/stderr chunks plus lifecycle transitions. Cursor-paginated. ```bash CLI theme={"theme":{"light":"github-light","dark":"github-dark"}} dedalus machines executions events --machine-id "$MACHINE_ID" --execution-id "$EXEC_ID" ``` ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} page = client.machines.executions.events(machine_id=machine_id, execution_id="") for e in page.items or []: print(e.sequence, e.type, e.chunk) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} const events = client.machines.executions.events({ machine_id: machineId, execution_id: "", }); for await (const e of events) console.log(e.sequence, e.type, e.chunk); ``` ```go Go theme={"theme":{"light":"github-light","dark":"github-dark"}} page, _ := client.Machines.Executions.Events(ctx, dedalus.MachineExecutionEventsParams{ MachineID: machineID, ExecutionID: "", }) for _, e := range page.Items { fmt.Println(e.Sequence, e.Type, e.Chunk) } ``` The machine that owns the execution. The execution whose events you want to read. Pagination cursor from a prior page's `next_cursor`. Max events per page. ## Output Output returns the command's captured `stdout` and `stderr` after the execution finishes. ```bash CLI theme={"theme":{"light":"github-light","dark":"github-dark"}} dedalus machines executions output --machine-id "$MACHINE_ID" --execution-id "$EXEC_ID" ``` ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} out = client.machines.executions.output(machine_id=machine_id, execution_id="") print(out.stdout) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} const out = await client.machines.executions.output({ machine_id: machineId, execution_id: "", }); console.log(out.stdout); ``` ```go Go theme={"theme":{"light":"github-light","dark":"github-dark"}} out, _ := client.Machines.Executions.Output(ctx, dedalus.MachineExecutionOutputParams{ MachineID: machineID, ExecutionID: "", }) fmt.Println(out.Stdout) ``` The machine that owns the execution. The finished execution whose output you want. # Lifecycle Source: https://docs.dedaluslabs.ai/dcs/dm/lifecycle Sleep, wake, and delete Dedalus Machines Machines have four states: **running**, **sleeping**, **starting**, and **destroyed**. You control transitions between them. ## List machines Returns every machine in your account, including sleeping machines. ```bash CLI theme={"theme":{"light":"github-light","dark":"github-dark"}} dedalus machines list ``` ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} machines = client.machines.list() ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} const machines = await client.machines.list(); ``` ```go Go theme={"theme":{"light":"github-light","dark":"github-dark"}} machines, _ := client.Machines.List(ctx, dedalus.MachineListParams{}) fmt.Println(len(machines.Items)) ``` ## Get machine details Returns a point-in-time snapshot of the machine object: ID, desired state, lifecycle status, CPU, memory, storage, and autosleep policy. ```bash CLI theme={"theme":{"light":"github-light","dark":"github-dark"}} dedalus machines retrieve --machine-id ``` ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} dm = client.machines.retrieve(machine_id="") ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} const dm = await client.machines.retrieve({ machine_id: "" }); ``` ```go Go theme={"theme":{"light":"github-light","dark":"github-dark"}} dm, _ := client.Machines.Get(ctx, dedalus.MachineGetParams{ MachineID: machineID, }) fmt.Println(dm.Status.Phase) ``` The machine to retrieve. ## Watch Streams state changes. Use it to monitor sleep, wake, and delete. The stream closes when the machine reaches a stable phase. ```bash CLI theme={"theme":{"light":"github-light","dark":"github-dark"}} dedalus machines watch --machine-id ``` ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} import json with client.machines.with_streaming_response.watch(machine_id="") as resp: for line in resp.iter_lines(): if line.startswith("data:"): payload = json.loads(line[5:]) print(payload["status"]["phase"]) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} const stream = await client.machines.watch({ machine_id: "" }); for await (const evt of stream) { console.log(evt.status.phase); } ``` ```go Go theme={"theme":{"light":"github-light","dark":"github-dark"}} stream := client.Machines.WatchStreaming(ctx, dedalus.MachineWatchParams{ MachineID: machineID, }) for stream.Next() { fmt.Println(stream.Current().Status.Phase) } ``` The machine to watch. ## Auto-sleep By default, machines auto-sleep after 5 idle minutes. Set `autosleep` on create or update. Use `never` to disable it. Activity includes executions, terminal traffic, SSH traffic, port traffic, and CPU work from user-owned processes inside the VM. ```bash CLI theme={"theme":{"light":"github-light","dark":"github-dark"}} dedalus machines update --machine-id --autosleep 15m ``` ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} client.machines.update( machine_id="", autosleep="15m", ) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} await client.machines.update({ machine_id: "", autosleep: "15m", }); ``` ```go Go theme={"theme":{"light":"github-light","dark":"github-dark"}} dm, _ := client.Machines.Update(ctx, dedalus.MachineUpdateParams{ MachineID: machineID, UpdateParams: dedalus.UpdateParams{ Autosleep: dedalus.String("15m"), }, }) fmt.Println(dm.AutosleepSeconds) ``` Idle window before auto-sleep. Accepts units like `30s`, `15m`, `2h`, `1w3d`, raw seconds like `"1800"`, or `never`. ## Sleep Sleeping machines have zero compute cost. Storage persists. Sleep preserves the machine's root filesystem. Paths such as `/etc`, `/home`, `/root`, `/usr/local`, and `/var` persist unless you mount something else over them. Kernel and runtime filesystems are recreated on each wake. Paths such as `/proc`, `/sys`, `/dev`, `/run`, and `/dev/shm` are not persistent storage. Treat `/tmp` as temporary scratch space; use `/var/tmp`, `/home`, `/root`, or another root-filesystem path for files you need after sleep. ```bash CLI theme={"theme":{"light":"github-light","dark":"github-dark"}} dedalus machines sleep --machine-id ``` ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} client.machines.sleep(machine_id="") ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} await client.machines.sleep({ machine_id: "" }); ``` ```go Go theme={"theme":{"light":"github-light","dark":"github-dark"}} client.Machines.Sleep(ctx, dedalus.MachineSleepParams{ MachineID: machineID, }) ``` ## Wake ```bash CLI theme={"theme":{"light":"github-light","dark":"github-dark"}} dedalus machines wake --machine-id ``` ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} client.machines.wake(machine_id="") ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} await client.machines.wake({ machine_id: "" }); ``` ```go Go theme={"theme":{"light":"github-light","dark":"github-dark"}} client.Machines.Wake(ctx, dedalus.MachineWakeParams{ MachineID: machineID, }) ``` The machine to sleep or wake. ## Delete Delete the machine. ```bash CLI theme={"theme":{"light":"github-light","dark":"github-dark"}} dedalus machines delete --machine-id ``` ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} client.machines.delete(machine_id="") ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} await client.machines.delete({ machine_id: "" }); ``` ```go Go theme={"theme":{"light":"github-light","dark":"github-dark"}} client.Machines.Delete(ctx, dedalus.MachineDeleteParams{ MachineID: machineID, }) ``` The machine to delete. Delete removes the machine from normal use immediately. Storage may be retained for up to 30 days. Contact support for recovery. # Ports Source: https://docs.dedaluslabs.ai/dcs/dm/ports Expose a port on a Dedalus Machine over a public HTTPS URL `ports` is the unreleased control-plane name. The CLI v0.5.0 and SDKs v0.4.0 ship this resource as `previews` and do not provide `machines.ports`. The examples below use the released namespace. ```bash CLI theme={"theme":{"light":"github-light","dark":"github-dark"}} dedalus machines previews create \ --machine-id \ --port 3000 \ --protocol https \ --visibility public ``` ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} p = client.machines.previews.create( machine_id="", port=3000, protocol="https", visibility="public", ) print(p.preview_id) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} const p = await client.machines.previews.create({ machine_id: "", port: 3000, protocol: "https", visibility: "public", }); console.log(p.preview_id); ``` ```go Go theme={"theme":{"light":"github-light","dark":"github-dark"}} p, _ := client.Machines.Previews.New(ctx, dedalus.MachinePreviewNewParams{ MachineID: machineID, PreviewCreateParams: dedalus.PreviewCreateParams{ Port: 3000, Protocol: dedalus.PreviewCreateParamsProtocolHTTPS, Visibility: dedalus.PreviewCreateParamsVisibilityPublic, }, }) fmt.Println(p.PreviewID) ``` Creates a URL that proxies to a port inside the machine. Creation can return `wake_in_progress`; retrieve the preview by `preview_id` until its status is `ready`, then use the API-returned `url`. Treat `closed`, `expired`, and `failed` as terminal errors. The machine that runs the service. The internal port your service listens on. Public protocol for the exposed URL. Who can use the URL: `public`, `org`, or `private`. ## Manage exposed ports ```bash CLI theme={"theme":{"light":"github-light","dark":"github-dark"}} dedalus machines previews list --machine-id dedalus machines previews retrieve --machine-id --preview-id dedalus machines previews delete --machine-id --preview-id ``` ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} client.machines.previews.list(machine_id="") client.machines.previews.retrieve( machine_id="", preview_id="", ) client.machines.previews.delete( machine_id="", preview_id="", ) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} await client.machines.previews.list({ machine_id: "" }); await client.machines.previews.retrieve({ machine_id: "", preview_id: "", }); await client.machines.previews.delete({ machine_id: "", preview_id: "", }); ``` ```go Go theme={"theme":{"light":"github-light","dark":"github-dark"}} client.Machines.Previews.List(ctx, dedalus.MachinePreviewListParams{ MachineID: machineID, }) client.Machines.Previews.Get(ctx, dedalus.MachinePreviewGetParams{ MachineID: machineID, PreviewID: "", }) client.Machines.Previews.Delete(ctx, dedalus.MachinePreviewDeleteParams{ MachineID: machineID, PreviewID: "", }) ``` The machine that owns the exposed port. The preview resource to retrieve or delete. # Dedalus Machines Pricing Source: https://docs.dedaluslabs.ai/dcs/dm/pricing Per-second compute, persistent storage, no idle tax Compute is billed per second, only while your machine is awake. Storage persists across sleep/wake and is included in your plans. ## Plans | | Hobby | Pro | Enterprise | | ------------ | ---------------------------- | ---------------------- | ---------- | | **Price** | \$0/mo | \$20/mo | Custom | | **Credits** | \$20 one-time sign-up credit | \$20/mo included usage | Custom | | **vCPU** | Up to 4 | Up to 16 | Custom | | **RAM** | Up to 16 GiB | Up to 64 GiB | Custom | | **Storage** | 10 GiB | 20 GiB (expandable) | Custom | | **Machines** | 5 | 20 | Unlimited | | **Compute** | 50 hrs/mo ceiling | Unlimited | Unlimited | [Contact us](mailto:support@dedaluslabs.ai) for Enterprise pricing. ## Compute rates Billed only when awake. | Resource | Per second | Per hour | | -------- | ------------ | --------- | | vCPU | \$0.0000126 | \$0.04536 | | GiB RAM | \$0.00000405 | \$0.01458 | ## Storage Included with your plan. Additional logical storage is \$0.08/GiB-month. ## How credits work Your Pro subscription **is** your usage credit. Not an additional charge on top. | Your usage | You pay | | ---------- | ---------------------------- | | Under \$20 | \$20 (just the subscription) | | Over \$20 | \$20 + the overage | Recurring plan credits reset each billing cycle. Hobby's \$20 sign-up credit is one-time and does not reset. | Dedalus Machine | Compute/hr | Hours covered | | --------------- | ---------- | ------------- | | 1 vCPU / 2 GiB | \$0.07452 | \~268 hrs | | 2 vCPU / 4 GiB | \$0.14904 | \~134 hrs | | 2 vCPU / 8 GiB | \$0.20736 | \~96 hrs | | 4 vCPU / 8 GiB | \$0.29808 | \~67 hrs | ## Startup program Up to \$25,000 in compute credits, valid for 12 months. Email us with your company name and use case. # Quickstart Source: https://docs.dedaluslabs.ai/dcs/dm/quickstart Use the Dedalus CLI to create a Dedalus Machine. Create a machine, run a command, retrieve output, then open SSH. ## CLI Control machines from a terminal, script, or agent. ```bash macOS/Linux theme={"theme":{"light":"github-light","dark":"github-dark"}} curl -fsSL https://raw.githubusercontent.com/dedalus-labs/dedalus-cli/main/scripts/install.sh | bash ``` ```bash Homebrew theme={"theme":{"light":"github-light","dark":"github-dark"}} brew install dedalus-labs/tap/dedalus ``` ```powershell Windows theme={"theme":{"light":"github-light","dark":"github-dark"}} irm https://raw.githubusercontent.com/dedalus-labs/dedalus-cli/main/scripts/install.ps1 | iex ``` ```bash GitHub theme={"theme":{"light":"github-light","dark":"github-dark"}} git clone https://github.com/dedalus-labs/dedalus-cli.git cd dedalus-cli go install ./cmd/dedalus ``` Get a Dedalus API key from the [Dashboard](https://www.dedaluslabs.ai/dashboard/api-keys) and set it in your shell. ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} export DEDALUS_API_KEY="dsk_your_api_key" ``` The CLI calls the [Machines API](/api-reference/dcs). ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} dedalus machines create ``` Every flag is optional. With no flags you get a 1 vCPU machine with 4096 MiB of memory, 10 GiB of storage, and a 5-minute autosleep window. Pass `--vcpu`, `--memory-mib`, `--storage-gib`, or `--autosleep` to override. Save the `machine_id`. Later commands use it. ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "machine_id": "dm-...", "vcpu": 1, "memory_mib": 4096, "storage_gib": 10, "autosleep_seconds": 300, "desired_state": "running", "status": { "phase": "accepted", "reason": "Accepted", "retryable": true, "revision": "1", "last_transition_at": "2026-06-18T00:00:00Z", "last_progress_at": "2026-06-18T00:00:00Z" } } ``` The CLI runs commands directly, without opening a shell. Machine IDs start with `dm-`. ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} dedalus machines executions create \ --machine-id dm-... \ --command '["/bin/bash", "-c", "whoami && uname -a"]' ``` Save the `execution_id`. Use it to retrieve the result. ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "execution_id": "wexec-...", "machine_id": "dm-...", "status": "queued", "command": ["/bin/bash", "-c", "whoami && uname -a"], "created_at": "2026-06-18T00:00:02Z" } ``` Poll the execution until its status is `succeeded`, then fetch the captured output. ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} MACHINE_ID=dm-... EXECUTION_ID=wexec-... dedalus machines executions retrieve \ --machine-id "$MACHINE_ID" \ --execution-id "$EXECUTION_ID" dedalus machines executions output \ --machine-id "$MACHINE_ID" \ --execution-id "$EXECUTION_ID" ``` ## SSH Use executions for agents. Use SSH for human shells and debugging. Install packages and run normal Linux commands. ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} dedalus ssh dm-... # Inside the machine: whoami && uname -a ``` ## SDK These examples create a machine, run a command, and print output. ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} import time from dedalus_sdk import Dedalus client = Dedalus() DONE = {"succeeded", "failed", "cancelled", "expired"} dm = client.machines.create(vcpu=1, memory_mib=1024, storage_gib=10) stream = client.machines.watch(machine_id=dm.machine_id) for _ in stream: pass exc = client.machines.executions.create( machine_id=dm.machine_id, command=["/bin/bash", "-c", "whoami && uname -a"], ) while exc.status not in DONE: time.sleep(0.5) exc = client.machines.executions.retrieve( machine_id=dm.machine_id, execution_id=exc.execution_id, ) if exc.status != "succeeded": raise RuntimeError(f"{exc.status}: {exc.error_code}: {exc.error_message}") out = client.machines.executions.output( machine_id=dm.machine_id, execution_id=exc.execution_id, ) print(out.stdout) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from "dedalus"; const client = new Dedalus(); const DONE = new Set(["succeeded", "failed", "cancelled", "expired"]); const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); const dm = await client.machines.create({ vcpu: 1, memory_mib: 1024, storage_gib: 10, }); const stream = await client.machines.watch({ machine_id: dm.machine_id }); for await (const _ of stream) {} let exc = await client.machines.executions.create({ machine_id: dm.machine_id, command: ["/bin/bash", "-c", "whoami && uname -a"], }); while (!DONE.has(exc.status)) { await sleep(500); exc = await client.machines.executions.retrieve({ machine_id: dm.machine_id, execution_id: exc.execution_id, }); } if (exc.status !== "succeeded") { throw new Error(`${exc.status}: ${exc.error_code}: ${exc.error_message}`); } const out = await client.machines.executions.output({ machine_id: dm.machine_id, execution_id: exc.execution_id, }); console.log(out.stdout); ``` ```go Go theme={"theme":{"light":"github-light","dark":"github-dark"}} package main import ( "context" "fmt" "time" dedalus "github.com/dedalus-labs/dedalus-go" ) func main() { client := dedalus.NewClient() ctx := context.Background() done := map[dedalus.ExecutionStatus]bool{ dedalus.ExecutionStatusSucceeded: true, dedalus.ExecutionStatusFailed: true, dedalus.ExecutionStatusCancelled: true, dedalus.ExecutionStatusExpired: true, } dm, err := client.Machines.New(ctx, dedalus.MachineNewParams{ CreateParams: dedalus.CreateParams{ VCPU: 1, MemoryMiB: 1024, StorageGiB: 10, }, }) check(err) stream := client.Machines.WatchStreaming(ctx, dedalus.MachineWatchParams{ MachineID: dm.MachineID, }) for stream.Next() { } check(stream.Err()) exc, err := client.Machines.Executions.New(ctx, dedalus.MachineExecutionNewParams{ MachineID: dm.MachineID, ExecutionCreateParams: dedalus.ExecutionCreateParams{ Command: []string{"/bin/bash", "-c", "whoami && uname -a"}, }, }) check(err) for !done[exc.Status] { time.Sleep(500 * time.Millisecond) exc, err = client.Machines.Executions.Get(ctx, dedalus.MachineExecutionGetParams{ MachineID: dm.MachineID, ExecutionID: exc.ExecutionID, }) check(err) } if exc.Status != dedalus.ExecutionStatusSucceeded { panic(fmt.Sprintf("%s: %s", exc.ErrorCode, exc.ErrorMessage)) } out, err := client.Machines.Executions.Output(ctx, dedalus.MachineExecutionOutputParams{ MachineID: dm.MachineID, ExecutionID: exc.ExecutionID, }) check(err) fmt.Println(out.Stdout) } func check(err error) { if err != nil { panic(err) } } ``` Next: sleep, wake, watch, or delete the machine in [Lifecycle](/dcs/dm/lifecycle). # SSH Source: https://docs.dedaluslabs.ai/dcs/dm/ssh Open an SSH session to a Dedalus Machine A session is a short-lived authorization to connect over real SSH. The machine wakes if it was sleeping. Use the CLI for interactive login. It creates the session, writes the returned user certificate and gateway host trust into temporary OpenSSH files, enables strict host checking, and launches `ssh`. ```bash CLI theme={"theme":{"light":"github-light","dark":"github-dark"}} dedalus ssh ``` ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} from pathlib import Path pubkey = Path("~/.ssh/id_ed25519.pub").expanduser().read_text().strip() sess = client.machines.ssh.create(machine_id="", public_key=pubkey) if not sess.connection: raise RuntimeError(f"SSH session is {sess.status}; poll until ready") print(sess.connection) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} import fs from "node:fs"; const publicKey = fs.readFileSync(`${process.env.HOME}/.ssh/id_ed25519.pub`, "utf8").trim(); const sess = await client.machines.ssh.create({ machine_id: "", public_key: publicKey, }); if (!sess.connection) throw new Error(`SSH session is ${sess.status}; poll until ready`); console.log(sess.connection); ``` ```go Go theme={"theme":{"light":"github-light","dark":"github-dark"}} pubkey, _ := os.ReadFile(filepath.Join(os.Getenv("HOME"), ".ssh", "id_ed25519.pub")) sess, _ := client.Machines.SSH.New(ctx, dedalus.MachineSSHNewParams{ MachineID: machineID, SSHSessionCreateParams: dedalus.SSHSessionCreateParams{ PublicKey: strings.TrimSpace(string(pubkey)), }, }) if !sess.JSON.Connection.Valid() { panic(fmt.Sprintf("SSH session is %s; poll until ready", sess.Status)) } fmt.Printf("%+v\n", sess.Connection) ``` The Python, TypeScript, and Go SDKs return the raw SSH session contract for custom tools. If you launch OpenSSH yourself, use `connection.user_certificate` as `CertificateFile` and `connection.host_trust` as an `@cert-authority` entry in `UserKnownHostsFile`; do not skip strict host checking. The machine to open SSH access to. Your OpenSSH public key, such as the contents of `~/.ssh/id_ed25519.pub`. # Terminals Source: https://docs.dedaluslabs.ai/dcs/dm/terminals PTY sessions on a Dedalus Machine, streamed over WebSocket A terminal is a PTY opened inside the machine, with stdin/stdout streamed to your client over a WebSocket. The machine wakes if it was sleeping. ```bash CLI theme={"theme":{"light":"github-light","dark":"github-dark"}} dedalus machines terminals create \ --machine-id \ --width 80 \ --height 24 \ --shell /bin/bash \ --cwd /root \ --env '{"FOO":"bar"}' ``` ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} term = client.machines.terminals.create( machine_id="", width=80, height=24, shell="/bin/bash", # optional cwd="/root", # optional env={"FOO": "bar"}, # optional ) print(term.stream_url) # wss://... ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} const term = await client.machines.terminals.create({ machine_id: "", width: 80, height: 24, }); console.log(term.stream_url); ``` ```go Go theme={"theme":{"light":"github-light","dark":"github-dark"}} term, _ := client.Machines.Terminals.New(ctx, dedalus.MachineTerminalNewParams{ MachineID: machineID, TerminalCreateParams: dedalus.TerminalCreateParams{ Width: 80, Height: 24, Shell: dedalus.String("/bin/bash"), Cwd: dedalus.String("/root"), Env: map[string]string{"FOO": "bar"}, }, }) fmt.Println(term.StreamURL) ``` The machine where the terminal opens. Initial terminal width in columns. Initial terminal height in rows. Shell to start inside the PTY. Working directory for the shell. Extra environment variables for the shell. ## Wire protocol | Direction | Frame | Payload | | --------------- | ----------- | ---------------------------------------------------------------- | | Client → server | Binary | Raw bytes written to the shell's stdin (keystrokes, paste, etc.) | | Client → server | Text (JSON) | `{"cols": 100, "rows": 30}` to resize the PTY | | Server → client | Binary | Raw bytes from the shell's stdout/stderr (ANSI escapes intact) | | Server → client | Close | Shell exited or session expired | ## Manage terminals ```bash CLI theme={"theme":{"light":"github-light","dark":"github-dark"}} dedalus machines terminals list --machine-id dedalus machines terminals retrieve --machine-id --terminal-id dedalus machines terminals delete --machine-id --terminal-id ``` ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} client.machines.terminals.list(machine_id="") client.machines.terminals.retrieve(machine_id="", terminal_id="") client.machines.terminals.delete(machine_id="", terminal_id="") ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} await client.machines.terminals.list({ machine_id: "" }); await client.machines.terminals.retrieve({ machine_id: "", terminal_id: "", }); await client.machines.terminals.delete({ machine_id: "", terminal_id: "", }); ``` ```go Go theme={"theme":{"light":"github-light","dark":"github-dark"}} client.Machines.Terminals.List(ctx, dedalus.MachineTerminalListParams{ MachineID: machineID, }) client.Machines.Terminals.Get(ctx, dedalus.MachineTerminalGetParams{ MachineID: machineID, TerminalID: "", }) client.Machines.Terminals.Delete(ctx, dedalus.MachineTerminalDeleteParams{ MachineID: machineID, TerminalID: "", }) ``` The machine that owns the terminal. The terminal to retrieve or close. # Overview Source: https://docs.dedaluslabs.ai/dcs/index The compute substrate for AI agents Dedalus Cloud Services (DCS) is the compute primitive for AI agents. Learn more about our flagship product offering, Dedalus Machines.
Products
Dedalus Shell
# Audio Source: https://docs.dedaluslabs.ai/sdk/agents/audio Speech-to-text, translation, and text-to-speech with the Dedalus SDK Build voice workflows with one SDK: transcribe audio, translate it to English, and synthesize speech. Use `openai/gpt-4o-transcribe` for transcription and `openai/gpt-4o-mini-tts` for speech generation. ## Speech to Text ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_labs import AsyncDedalus client = AsyncDedalus() with open("audio.mp3", "rb") as audio_file: transcription = await client.audio.transcriptions.create( model="openai/gpt-4o-transcribe", file=audio_file, ) print(transcription.text) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from "dedalus-labs"; import fs from "node:fs"; const client = new Dedalus(); const transcription = await client.audio.transcriptions.create({ model: "openai/gpt-4o-transcribe", file: fs.createReadStream("audio.mp3"), }); console.log(transcription.text); ``` ## Audio Translation ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_labs import AsyncDedalus client = AsyncDedalus() with open("spanish-audio.mp3", "rb") as audio_file: translation = await client.audio.translations.create( model="openai/gpt-4o-mini-transcribe", file=audio_file, ) print(translation.text) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from "dedalus-labs"; import fs from "node:fs"; const client = new Dedalus(); const translation = await client.audio.translations.create({ model: "openai/gpt-4o-mini-transcribe", file: fs.createReadStream("spanish-audio.mp3"), }); console.log(translation.text); ``` ## Text to Speech ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_labs import AsyncDedalus client = AsyncDedalus() speech = await client.audio.speech.create( model="openai/gpt-4o-mini-tts", voice="alloy", input="Hello from Dedalus", ) speech.stream_to_file("speech.mp3") ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from "dedalus-labs"; import fs from "node:fs"; const client = new Dedalus(); const response = await client.audio.speech.create({ model: "openai/gpt-4o-mini-tts", voice: "alloy", input: "Hello from Dedalus", }); fs.writeFileSync("speech.mp3", Buffer.from(await response.arrayBuffer())); ``` ## API endpoints POST /v1/audio/speech POST /v1/audio/transcriptions POST /v1/audio/translations # Bring Your Own Key (BYOK) Source: https://docs.dedaluslabs.ai/sdk/agents/byok Use your own API keys to call providers directly through Dedalus BYOK lets you send requests through Dedalus using your own provider API key. The request still flows through our unified API (routing, tool calling, streaming, format normalization), but the LLM call is billed to your account with the provider. ## When to use BYOK * You have negotiated pricing or credits with a provider. * You want to use a model tier or region not available on our shared keys. * Your compliance policy requires that API keys stay under your control. ## Quick start Pass three headers (or SDK options) alongside your normal Dedalus API key: | Header | SDK option | Description | | ------------------ | ---------------- | ----------------------------------------------------- | | `X-Provider` | `provider` | Provider name (`openai`, `anthropic`, `google`, etc.) | | `X-Provider-Key` | `provider_key` | Your API key for that provider | | `X-Provider-Model` | `provider_model` | Model identifier at the provider (optional) | Only `X-Provider-Key` is strictly required. If you omit `X-Provider`, it is inferred from the model name. If you omit `X-Provider-Model`, the model from the request body is used. ## Examples ### curl ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} curl https://api.dedaluslabs.ai/v1/chat/completions \ -H "Authorization: Bearer $DEDALUS_API_KEY" \ -H "X-Provider: openai" \ -H "X-Provider-Key: $OPENAI_API_KEY" \ -H "X-Provider-Model: gpt-4o" \ -H "Content-Type: application/json" \ -d '{ "model": "openai/gpt-4o", "messages": [{"role": "user", "content": "Hello"}] }' ``` ### Python SDK ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_labs import AsyncDedalus client = AsyncDedalus( provider="openai", provider_key="sk-your-openai-key", provider_model="gpt-4o", ) response = await client.chat.completions.create( model="openai/gpt-4o", messages=[{"role": "user", "content": "Hello"}], ) ``` ### TypeScript SDK ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from "dedalus-labs"; const client = new Dedalus({ provider: "openai", providerKey: "sk-your-openai-key", providerModel: "gpt-4o", }); const response = await client.chat.completions.create({ model: "openai/gpt-4o", messages: [{ role: "user", content: "Hello" }], }); ``` ### Environment variables You can also set BYOK options via environment variables instead of passing them in code: ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} export DEDALUS_PROVIDER="anthropic" export DEDALUS_PROVIDER_KEY="sk-ant-your-key" export DEDALUS_PROVIDER_MODEL="claude-sonnet-4-5-20250929" ``` The SDK picks these up automatically. No code changes needed. ## Per-request overrides The SDK options set defaults for every request. You can also override per-request by setting the headers directly: ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} response = await client.chat.completions.create( model="google/gemini-2.5-pro", messages=[{"role": "user", "content": "Hello"}], extra_headers={ "X-Provider": "google", "X-Provider-Key": "your-google-key", }, ) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} const response = await client.chat.completions.create( { model: "google/gemini-2.5-pro", messages: [{ role: "user", content: "Hello" }], }, { headers: { "X-Provider": "google", "X-Provider-Key": "your-google-key", }, }, ); ``` ## Supported providers Any provider in our [model list](/sdk/agents/guides/providers) works with BYOK: openai anthropic google xai mistral deepseek groq cohere perplexity cerebras together\_ai fireworks\_ai moonshot ## How it works Your request still goes through Dedalus. We handle routing, format normalization, streaming, and tool calling. The only difference is which API key is used for the upstream LLM call. ``` You → Dedalus API (your Dedalus key) → Provider (your provider key) → Response → You ``` BYOK keys are sent over HTTPS and are never stored. They are used for the duration of the request and discarded. If you need Dedalus to manage keys on your behalf, contact us at [support@dedaluslabs.ai](mailto:support@dedaluslabs.ai). ## Error handling | Scenario | What happens | | ------------------------------- | -------------------------------------------------- | | Invalid provider name | HTTP 400 with supported provider list | | Missing or invalid provider key | Provider returns its own auth error (usually 401) | | Model not available on provider | Provider returns its own model error (usually 404) | The error response always includes the upstream provider's error message so you can debug directly. # Chat Source: https://docs.dedaluslabs.ai/sdk/agents/chat Send messages and get responses from any model The core of the Dedalus SDK: send a message, get a response. Works with any model from any provider. ## Start with chat ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio from dedalus_labs import AsyncDedalus, DedalusRunner from dotenv import load_dotenv load_dotenv() async def main(): client = AsyncDedalus() runner = DedalusRunner(client) response = await runner.run( input=( "I want to find the nearest basketball games in January in San Francisco.\n\n" "For now, do NOT make up events. Instead:\n" "1) Ask any clarifying questions you need.\n" "2) Propose a short plan for how you would find events.\n" "3) List the fields you'd extract for each event (for a table later)." ), model="anthropic/claude-opus-4-5", ) print(response.final_output) if __name__ == "__main__": asyncio.run(main()) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from "dedalus-labs"; import { DedalusRunner } from "dedalus-labs"; const client = new Dedalus(); const runner = new DedalusRunner(client); async function main() { const response = await runner.run({ input: "I want to find the nearest basketball games in January in San Francisco.\n\n" + "For now, do NOT make up events. Instead:\n" + "1) Ask any clarifying questions you need.\n" + "2) Propose a short plan for how you would find events.\n" + "3) List the fields you'd extract for each event (for a table later).", model: "anthropic/claude-opus-4-5", }); console.log(response.finalOutput); } main(); ``` ## Next steps * **Add actions**: [Tools](/sdk/agents/tools) — Let the model call your functions * **Route across models**: [Handoffs](/sdk/agents/handoffs) — Route to different models by phase * **Stream the workflow**: [Streaming](/sdk/agents/streaming) — Show progress in real time [Connect these docs programmatically](/ai-optimizations/using-mintlify-mcp) to Claude, VSCode, and more via MCP for real-time answers. # Go SDK Source: https://docs.dedaluslabs.ai/sdk/agents/go Agents API Go SDK v0.1.0-alpha.3 **v0.1.0-alpha.3** | [GitHub](https://github.com/dedalus-labs/dedalus-sdk-go) | [Changelog](https://github.com/dedalus-labs/dedalus-sdk-go/blob/main/CHANGELOG.md) ## Installation ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} go get github.com/dedalus-labs/dedalus-sdk-go ``` This library requires Go 1.22+. ## Usage See the full method reference in the [API Reference](/api-reference/dcs) tab. ```go theme={"theme":{"light":"github-light","dark":"github-dark"}} package main import ( "context" "fmt" "github.com/dedalus-labs/dedalus-sdk-go" "github.com/dedalus-labs/dedalus-sdk-go/option" ) func main() { client := githubcomdedaluslabsdedalussdkgo.NewClient( option.WithAPIKey("My API Key"), // defaults to os.LookupEnv("DEDALUS_API_KEY") option.WithEnvironmentDevelopment(), // defaults to option.WithEnvironmentProduction() ) completion, err := client.Chat.Completions.New(context.TODO(), githubcomdedaluslabsdedalussdkgo.ChatCompletionNewParams{ Messages: githubcomdedaluslabsdedalussdkgo.ChatCompletionNewParamsMessagesUnion{ OfMapOfAnyMap: []map[string]any{{ "role": "user", "content": "Hello, how are you today?", }}, }, Model: githubcomdedaluslabsdedalussdkgo.ChatCompletionNewParamsModelUnion{ OfModelID: githubcomdedaluslabsdedalussdkgo.String("openai/gpt-5"), }, }) if err != nil { panic(err.Error()) } fmt.Printf("%+v\n", completion.ID) } ``` ### Request fields The githubcomdedaluslabsdedalussdkgo library uses the [`omitzero`](https://tip.golang.org/doc/go1.24#encodingjsonpkgencodingjson) semantics from the Go 1.24+ `encoding/json` release for request fields. Required primitive fields (`int64`, `string`, etc.) feature the tag \`json:"...,required"\`. These fields are always serialized, even their zero values. Optional primitive types are wrapped in a `param.Opt[T]`. These fields can be set with the provided constructors, `githubcomdedaluslabsdedalussdkgo.String(string)`, `githubcomdedaluslabsdedalussdkgo.Int(int64)`, etc. Any `param.Opt[T]`, map, slice, struct or string enum uses the tag \`json:"...,omitzero"\`. Its zero value is considered omitted. The `param.IsOmitted(any)` function can confirm the presence of any `omitzero` field. ```go theme={"theme":{"light":"github-light","dark":"github-dark"}} p := githubcomdedaluslabsdedalussdkgo.ExampleParams{ ID: "id_xxx", // required property Name: githubcomdedaluslabsdedalussdkgo.String("..."), // optional property Point: githubcomdedaluslabsdedalussdkgo.Point{ X: 0, // required field will serialize as 0 Y: githubcomdedaluslabsdedalussdkgo.Int(1), // optional field will serialize as 1 // ... omitted non-required fields will not be serialized }, Origin: githubcomdedaluslabsdedalussdkgo.Origin{}, // the zero value of [Origin] is considered omitted } ``` To send `null` instead of a `param.Opt[T]`, use `param.Null[T]()`. To send `null` instead of a struct `T`, use `param.NullStruct[T]()`. ```go theme={"theme":{"light":"github-light","dark":"github-dark"}} p.Name = param.Null[string]() // 'null' instead of string p.Point = param.NullStruct[Point]() // 'null' instead of struct param.IsNull(p.Name) // true param.IsNull(p.Point) // true ``` Request structs contain a `.SetExtraFields(map[string]any)` method which can send non-conforming fields in the request body. Extra fields overwrite any struct fields with a matching key. For security reasons, only use `SetExtraFields` with trusted data. To send a custom value instead of a struct, use `param.Override[T](value)`. ```go theme={"theme":{"light":"github-light","dark":"github-dark"}} // In cases where the API specifies a given type, // but you want to send something else, use [SetExtraFields]: p.SetExtraFields(map[string]any{ "x": 0.01, // send "x" as a float instead of int }) // Send a number instead of an object custom := param.Override[githubcomdedaluslabsdedalussdkgo.FooParams](12) ``` ### Request unions Unions are represented as a struct with fields prefixed by "Of" for each of its variants, only one field can be non-zero. The non-zero field will be serialized. Sub-properties of the union can be accessed via methods on the union struct. These methods return a mutable pointer to the underlying data, if present. ```go theme={"theme":{"light":"github-light","dark":"github-dark"}} // Only one field can be non-zero, use param.IsOmitted() to check if a field is set type AnimalUnionParam struct { OfCat *Cat `json:",omitzero,inline` OfDog *Dog `json:",omitzero,inline` } animal := AnimalUnionParam{ OfCat: &Cat{ Name: "Whiskers", Owner: PersonParam{ Address: AddressParam{Street: "3333 Coyote Hill Rd", Zip: 0}, }, }, } // Mutating a field if address := animal.GetOwner().GetAddress(); address != nil { address.ZipCode = 94304 } ``` ### Response objects All fields in response structs are ordinary value types (not pointers or wrappers). Response structs also include a special `JSON` field containing metadata about each property. ```go theme={"theme":{"light":"github-light","dark":"github-dark"}} type Animal struct { Name string `json:"name,nullable"` Owners int `json:"owners"` Age int `json:"age"` JSON struct { Name respjson.Field Owner respjson.Field Age respjson.Field ExtraFields map[string]respjson.Field } `json:"-"` } ``` To handle optional data, use the `.Valid()` method on the JSON field. `.Valid()` returns true if a field is not `null`, not present, or couldn't be marshaled. If `.Valid()` is false, the corresponding field will simply be its zero value. ```go theme={"theme":{"light":"github-light","dark":"github-dark"}} raw := `{"owners": 1, "name": null}` var res Animal json.Unmarshal([]byte(raw), &res) // Accessing regular fields res.Owners // 1 res.Name // "" res.Age // 0 // Optional field checks res.JSON.Owners.Valid() // true res.JSON.Name.Valid() // false res.JSON.Age.Valid() // false // Raw JSON values res.JSON.Owners.Raw() // "1" res.JSON.Name.Raw() == "null" // true res.JSON.Name.Raw() == respjson.Null // true res.JSON.Age.Raw() == "" // true res.JSON.Age.Raw() == respjson.Omitted // true ``` These `.JSON` structs also include an `ExtraFields` map containing any properties in the json response that were not specified in the struct. This can be useful for API features not yet present in the SDK. ```go theme={"theme":{"light":"github-light","dark":"github-dark"}} body := res.JSON.ExtraFields["my_unexpected_field"].Raw() ``` ### Response Unions In responses, unions are represented by a flattened struct containing all possible fields from each of the object variants. To convert it to a variant use the `.AsFooVariant()` method or the `.AsAny()` method if present. If a response value union contains primitive values, primitive fields will be alongside the properties but prefixed with `Of` and feature the tag `json:"...,inline"`. ```go theme={"theme":{"light":"github-light","dark":"github-dark"}} type AnimalUnion struct { // From variants [Dog], [Cat] Owner Person `json:"owner"` // From variant [Dog] DogBreed string `json:"dog_breed"` // From variant [Cat] CatBreed string `json:"cat_breed"` // ... JSON struct { Owner respjson.Field // ... } `json:"-"` } // If animal variant if animal.Owner.Address.ZipCode == "" { panic("missing zip code") } // Switch on the variant switch variant := animal.AsAny().(type) { case Dog: case Cat: default: panic("unexpected type") } ``` ### RequestOptions This library uses the functional options pattern. Functions defined in the `option` package return a `RequestOption`, which is a closure that mutates a `RequestConfig`. These options can be supplied to the client or at individual requests. For example: ```go theme={"theme":{"light":"github-light","dark":"github-dark"}} client := githubcomdedaluslabsdedalussdkgo.NewClient( // Adds a header to every request made by the client option.WithHeader("X-Some-Header", "custom_header_info"), ) client.Health.Check(context.TODO(), ..., // Override the header option.WithHeader("X-Some-Header", "some_other_custom_header_info"), // Add an undocumented field to the request body, using sjson syntax option.WithJSONSet("some.json.path", map[string]string{"my": "object"}), ) ``` The request option `option.WithDebugLog(nil)` may be helpful while debugging. See the [full list of request options](https://pkg.go.dev/github.com/dedalus-labs/dedalus-sdk-go/option). ### Pagination This library provides some conveniences for working with paginated list endpoints. You can use `.ListAutoPaging()` methods to iterate through items across all pages: Or you can use simple `.List()` methods to fetch a single page and receive a standard response object with additional helper methods like `.GetNextPage()`, e.g.: ### Errors When the API returns a non-success status code, we return an error with type `*githubcomdedaluslabsdedalussdkgo.Error`. This contains the `StatusCode`, `*http.Request`, and `*http.Response` values of the request, as well as the JSON of the error body (much like other response objects in the SDK). To handle errors, we recommend that you use the `errors.As` pattern: ```go theme={"theme":{"light":"github-light","dark":"github-dark"}} _, err := client.Health.Check(context.TODO()) if err != nil { var apierr *githubcomdedaluslabsdedalussdkgo.Error if errors.As(err, &apierr) { println(string(apierr.DumpRequest(true))) // Prints the serialized HTTP request println(string(apierr.DumpResponse(true))) // Prints the serialized HTTP response } panic(err.Error()) // GET "/health": 400 Bad Request { ... } } ``` When other errors occur, they are returned unwrapped; for example, if HTTP transport fails, you might receive `*url.Error` wrapping `*net.OpError`. ### Timeouts Requests do not time out by default; use context to configure a timeout for a request lifecycle. Note that if a request is [retried](#retries), the context timeout does not start over. To set a per-retry timeout, use `option.WithRequestTimeout()`. ```go theme={"theme":{"light":"github-light","dark":"github-dark"}} // This sets the timeout for the request, including all the retries. ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) defer cancel() client.Health.Check( ctx, // This sets the per-retry timeout option.WithRequestTimeout(20*time.Second), ) ``` ### File uploads Request parameters that correspond to file uploads in multipart requests are typed as `io.Reader`. The contents of the `io.Reader` will by default be sent as a multipart form part with the file name of "anonymous\_file" and content-type of "application/octet-stream". The file name and content-type can be customized by implementing `Name() string` or `ContentType() string` on the run-time type of `io.Reader`. Note that `os.File` implements `Name() string`, so a file returned by `os.Open` will be sent with the file name on disk. We also provide a helper `githubcomdedaluslabsdedalussdkgo.File(reader io.Reader, filename string, contentType string)` which can be used to wrap any `io.Reader` with the appropriate file name and content type. ```go theme={"theme":{"light":"github-light","dark":"github-dark"}} // A file from the file system file, err := os.Open("/path/to/file") githubcomdedaluslabsdedalussdkgo.AudioTranscriptionNewParams{ File: file, Model: "model", } // A file from a string githubcomdedaluslabsdedalussdkgo.AudioTranscriptionNewParams{ File: strings.NewReader("my file contents"), Model: "model", } // With a custom filename and contentType githubcomdedaluslabsdedalussdkgo.AudioTranscriptionNewParams{ File: githubcomdedaluslabsdedalussdkgo.File(strings.NewReader(`{"hello": "foo"}`), "file.go", "application/json"), Model: "model", } ``` ### Retries Certain errors will be automatically retried 2 times by default, with a short exponential backoff. We retry by default all connection errors, 408 Request Timeout, 409 Conflict, 429 Rate Limit, and >=500 Internal errors. You can use the `WithMaxRetries` option to configure or disable this: ```go theme={"theme":{"light":"github-light","dark":"github-dark"}} // Configure the default for all requests: client := githubcomdedaluslabsdedalussdkgo.NewClient( option.WithMaxRetries(0), // default is 2 ) // Override per-request: client.Health.Check(context.TODO(), option.WithMaxRetries(5)) ``` ### Accessing raw response data (e.g. response headers) You can access the raw HTTP response data by using the `option.WithResponseInto()` request option. This is useful when you need to examine response headers, status codes, or other details. ```go theme={"theme":{"light":"github-light","dark":"github-dark"}} // Create a variable to store the HTTP response var response *http.Response response, err := client.Health.Check(context.TODO(), option.WithResponseInto(&response)) if err != nil { // handle error } fmt.Printf("%+v\n", response) fmt.Printf("Status Code: %d\n", response.StatusCode) fmt.Printf("Headers: %+#v\n", response.Header) ``` ### Making custom/undocumented requests This library is typed for convenient access to the documented API. If you need to access undocumented endpoints, params, or response properties, the library can still be used. #### Undocumented endpoints To make requests to undocumented endpoints, you can use `client.Get`, `client.Post`, and other HTTP verbs. `RequestOptions` on the client, such as retries, will be respected when making these requests. ```go theme={"theme":{"light":"github-light","dark":"github-dark"}} var ( // params can be an io.Reader, a []byte, an encoding/json serializable object, // or a "…Params" struct defined in this library. params map[string]any // result can be an []byte, *http.Response, a encoding/json deserializable object, // or a model defined in this library. result *http.Response ) err := client.Post(context.Background(), "/unspecified", params, &result) if err != nil { … } ``` #### Undocumented request params To make requests using undocumented parameters, you may use either the `option.WithQuerySet()` or the `option.WithJSONSet()` methods. ```go theme={"theme":{"light":"github-light","dark":"github-dark"}} params := FooNewParams{ ID: "id_xxxx", Data: FooNewParamsData{ FirstName: githubcomdedaluslabsdedalussdkgo.String("John"), }, } client.Foo.New(context.Background(), params, option.WithJSONSet("data.last_name", "Doe")) ``` #### Undocumented response properties To access undocumented response properties, you may either access the raw JSON of the response as a string with `result.JSON.RawJSON()`, or get the raw JSON of a particular field on the result with `result.JSON.Foo.Raw()`. Any fields that are not present on the response struct will be saved and can be accessed by `result.JSON.ExtraFields()` which returns the extra fields as a `map[string]Field`. ### Middleware We provide `option.WithMiddleware` which applies the given middleware to requests. ```go theme={"theme":{"light":"github-light","dark":"github-dark"}} func Logger(req *http.Request, next option.MiddlewareNext) (res *http.Response, err error) { // Before the request start := time.Now() LogReq(req) // Forward the request to the next handler res, err = next(req) // Handle stuff after the request end := time.Now() LogRes(res, err, start - end) return res, err } client := githubcomdedaluslabsdedalussdkgo.NewClient( option.WithMiddleware(Logger), ) ``` When multiple middlewares are provided as variadic arguments, the middlewares are applied left to right. If `option.WithMiddleware` is given multiple times, for example first in the client then the method, the middleware in the client will run first and the middleware given in the method will run next. You may also replace the default `http.Client` with `option.WithHTTPClient(client)`. Only one http client is accepted (this overwrites any previous client) and receives requests after any middleware has been applied. ## Semantic versioning This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions: 1. Changes to library internals which are technically public but not intended or documented for external use. *(Please open a GitHub issue to let us know if you are relying on such internals.)* 2. Changes that we do not expect to impact the vast majority of users in practice. We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience. We are keen for your feedback; please open an [issue](https://www.github.com/dedalus-labs/dedalus-sdk-go/issues) with questions, bugs, or suggestions. # FAQ Source: https://docs.dedaluslabs.ai/sdk/agents/guides/faq Frequently Asked Questions * We make it easy to build complex AI agents with just 5 (or so) lines of code. * Agents built with our Dedalus SDK can connect to any MCP server on our marketplace, switch between any model provider, and even execute locally-defined tools. * Don’t yet see an MCP you want to use on our marketplace? Upload any MCP server and we’ll host it for free. Log into your [dashboard](https://www.dedaluslabs.ai/dashboard/api-keys) to get your API key. Yes! However, you don't need to. With a `DEDALUS_API_KEY` in your environment, we take care of routing to any provider or model for you, including handoffs between models from different providers. For an example, see our [handoffs](/sdk/agents/handoffs) page. Our SDK is currently available for Python and TypeScript. We recommend writing your MCPs in Python under Dedalus framework for speed and security. We also accept MCP servers written in other Python and TypeScript frameworks with HTTP transport. For best practices in writing MCP servers see our [server guidelines](/sdk/agents/guides/server-guidelines). Yes. Dedalus invented the most secure MCP auth framework. On the Dedalus marketplace/runner, authentication is handled via **DAuth** (our managed OAuth 2.1 flow) so agents can securely connect to protected MCP servers and external APIs without hardcoding secrets. See [authorization](/sdk/mcp/python/authorization) and the client auth guides ([bearer auth](/sdk/mcp/python/client/bearer-auth), [OAuth](/sdk/mcp/python/client/oauth)). Send us an email at [support@dedaluslabs.ai](mailto:support@dedaluslabs.ai) or send a message in our [Discord](https://discord.gg/RuDhZKnq5R). # Model Providers Source: https://docs.dedaluslabs.ai/sdk/agents/guides/providers Mix and match models from supported providers. `OPENAI_API_KEY` `ANTHROPIC_API_KEY` `GOOGLE_API_KEY` `FIREWORKS_API_KEY` `XAI_API_KEY` `PERPLEXITY_API_KEY` `DEEPSEEK_API_KEY` `GROQ_API_KEY` `COHERE_API_KEY` `CEREBRAS_API_KEY` `MISTRAL_API_KEY` `MOONSHOT_API_KEY` ## Model Recommendations by Use Case Choosing the right model depends on your specific requirements. Here's a guide to help you select the best provider and model for your needs: ### Tool Calling & Function Use **Best for:** Building agents and applications that need to call external tools or functions * `anthropic/claude-opus-4-5` - Excellent tool calling reliability with structured outputs * `anthropic/claude-sonnet-4-5-20250929` - Strong tool use with fast performance * `openai/gpt-5` - Native function calling support with structured responses * `openai/gpt-4o` - Reliable tool calling for production applications * `deepseek/deepseek-chat` - Advanced tool use with multi-step reasoning ### Coding & Development **Best for:** Code generation, debugging, and technical implementations * `deepseek/deepseek-coder` - Purpose-built for coding tasks * `openai/gpt-5-codex` - Specialized for code generation and completion * `anthropic/claude-opus-4-5` - Strong code understanding and generation * `anthropic/claude-sonnet-4-5-20250929` - Excellent coding with faster responses * `xai/grok-code-fast-1` - Fast code-focused model ### Reasoning & Complex Problem Solving **Best for:** Mathematical reasoning, logical analysis, and complex decision-making * `anthropic/claude-opus-4-5` - Advanced reasoning capabilities * `openai/o3` - Deep reasoning for complex problems * `openai/o1` - Strong multi-step reasoning * `deepseek/deepseek-reasoner` - Specialized reasoning model * `xai/grok-4-fast-reasoning` - Optimized for reasoning tasks ### Speed & Efficiency **Best for:** High-throughput applications requiring fast responses * `anthropic/claude-haiku-4-5-20251001` - Fast performance at lower cost * `google/gemini-2.5-flash` - Optimized for throughput and low latency * `openai/gpt-5-mini` - Lightweight, fast model * `openai/gpt-5-nano` - Ultra-fast for simple tasks * `xai/grok-4-fast-non-reasoning` - Quick responses without extended reasoning ### Long Context Tasks **Best for:** Processing large documents, codebases, or extended conversations * `google/gemini-2.5-pro` - Up to 1M+ token context window * `google/gemini-2.0-flash` - Large context with fast performance * `anthropic/claude-opus-4-5` - Extended context for complex analysis * `anthropic/claude-sonnet-4-5-20250929` - Strong long-context capabilities * `openai/gpt-4-32k` - Extended 32K context window ### Vision & Multimodal **Best for:** Image understanding, document analysis, and visual tasks * `openai/gpt-4o` - Strong vision capabilities with chat * `anthropic/claude-opus-4-5` - Advanced multimodal understanding * `anthropic/claude-sonnet-4-5-20250929` - Multimodal with fast performance * `google/gemini-2.5-pro` - Advanced vision and multimodal processing * `xai/grok-2-vision-1212` - Multimodal understanding Many providers offer multiple model tiers (e.g., mini, standard, pro, opus) that balance cost, speed, and capability. Start with smaller models for testing and scale up based on your performance requirements. ## Supported Models **Programmatic Discovery:** Use [`GET /v1/models`](/api/list-models) to list all hundreds models with capabilities (vision, tools, thinking, streaming) and routing metadata. Perfect for building model selectors or auto-populating dropdowns in tools like n8n. ### OpenAI #### Chat Models * `openai/gpt-5.2` * `openai/gpt-5.1` * `openai/gpt-5` * `openai/gpt-5-mini` * `openai/gpt-5-nano` * `openai/gpt-5-chat-latest` * `openai/gpt-4.1` * `openai/gpt-4.1-mini` * `openai/gpt-4.1-nano` * `openai/gpt-4o` * `openai/gpt-4o-2024-05-13` * `openai/gpt-5.2` * `openai/gpt-4o-search-preview` * `openai/gpt-4o-mini-search-preview` * `openai/chatgpt-4o-latest` * `openai/gpt-4-turbo` * `openai/gpt-4-turbo-2024-04-09` * `openai/gpt-4` * `openai/gpt-4-0125-preview` * `openai/gpt-4-1106-preview` * `openai/gpt-4-0613` * `openai/gpt-3.5-turbo` * `openai/gpt-3.5-turbo-0125` * `openai/gpt-3.5-turbo-1106` #### Reasoning Models * `openai/o1` * `openai/o3` * `openai/o3-mini` * `openai/o4-mini` #### Image Generation * `openai/dall-e-3` #### Audio Transcription * `openai/whisper-1` #### Embedding Models | Model | Price | | ------------------------------- | ------------------ | | `openai/text-embedding-3-large` | \$0.13 / 1M tokens | | `openai/text-embedding-3-small` | \$0.02 / 1M tokens | | `openai/text-embedding-ada-002` | \$0.10 / 1M tokens | ### Anthropic (Claude) #### Claude 4.6 Series * `anthropic/claude-opus-4-6` #### Claude 4.5 Series * `anthropic/claude-opus-4-5` * `anthropic/claude-haiku-4-5-20251001` * `anthropic/claude-sonnet-4-5-20250929` #### Claude 4 Series * `anthropic/claude-opus-4-1-20250805` * `anthropic/claude-opus-4-20250514` * `anthropic/claude-opus-4-5` #### Claude 3.7 Series * `anthropic/claude-3-7-sonnet-20250219` #### Claude 3.5 Series * `anthropic/claude-3-5-haiku-20241022` #### Claude 3 Series * `anthropic/claude-3-haiku-20240307` ### Google (Gemini) #### Gemini 3 Series * `google/gemini-3-pro-preview` * `google/gemini-3-flash-preview` #### Gemini 2.5 Series * `google/gemini-2.5-pro` * `google/gemini-2.5-flash` * `google/gemini-2.5-flash-lite` #### Gemini 2.0 Series * `google/gemini-2.0-flash` * `google/gemini-2.0-flash-exp` * `google/gemini-2.0-flash-001` * `google/gemini-2.0-flash-lite` #### Embedding Models * `google/text-embedding-004` ### xAI (Grok) #### Grok 4 Series * `xai/grok-4-1-fast-reasoning` * `xai/grok-4-1-fast-non-reasoning` * `xai/grok-4-fast-reasoning` * `xai/grok-4-fast-non-reasoning` * `xai/grok-code-fast-1` * `xai/grok-4-0709` #### Grok 3 Series * `xai/grok-3` * `xai/grok-3-mini` #### Grok 2 Series * `xai/grok-2-vision-1212` ### DeepSeek * `deepseek/deepseek-chat` * `deepseek/deepseek-reasoner` * `deepseek/deepseek-coder` ### Mistral * `mistral/mistral-large-latest` * `mistral/mistral-medium-latest` * `mistral/mistral-small-latest` * `mistral/codestral-2508` * `mistral/open-mistral-nemo-2407` * `mistral/pixtral-12b` ### Groq Lightning-fast inference for open source models. * `groq/llama-3.1-8b-instant` * `groq/llama-3.3-70b-versatile` * `groq/openai/gpt-oss-120b` * `groq/openai/gpt-oss-20b` * `groq/whisper-large-v3` * `groq/whisper-large-v3-turbo` ### Cerebras Ultra-fast inference on custom silicon. #### Production Models * `cerebras/llama3.1-8b` * `cerebras/llama-3.3-70b` * `cerebras/gpt-oss-120b` * `cerebras/qwen-3-32b` #### Preview Models * `cerebras/qwen-3-235b-a22b-instruct-2507` * `cerebras/zai-glm-4.7` ### Moonshot (Kimi) Advanced reasoning and extended context from Moonshot AI. * `moonshot/kimi-k2.5` * `moonshot/kimi-k2-0905-preview` * `moonshot/kimi-k2-0711-preview` * `moonshot/kimi-k2-turbo-preview` * `moonshot/kimi-k2-thinking` * `moonshot/kimi-k2-thinking-turbo` # MCP server guidelines Source: https://docs.dedaluslabs.ai/sdk/agents/guides/server-guidelines Best practices for building MCP servers that work well with the Dedalus SDK This guide covers practical best practices for building MCP servers that work reliably with the Dedalus SDK. If you’re new to MCP, start with the MCP docs first: * **Build a server**: [MCP server overview](/sdk/mcp/python/server/overview) * **Deploy**: [Deploy an MCP server](/sdk/mcp/python/deploy) * **Test/debug**: [Testing](/sdk/mcp/python/testing), [Debugging](/sdk/mcp/python/debugging) ## Guidelines (high-level) * **Keep tools small and deterministic**: One tool should do one job, with clear input/output. * **Use strict schemas**: Prefer explicit parameter types and avoid “any”-shaped payloads. * **Return stable data**: Avoid embedding large prose in tool responses—return structured fields whenever possible. * **Be stateless when possible**: It simplifies scaling and avoids surprising cross-user behavior. * **Handle errors explicitly**: Return actionable error messages; avoid silent failures. ## Next steps Connect from the Dedalus SDK Combine with local tools # Handoffs Source: https://docs.dedaluslabs.ai/sdk/agents/handoffs Route tasks to different models based on their strengths Different models excel at different tasks. GPT handles reasoning and tool use well. Claude writes better prose. Specialized models exist for code, math, and domain-specific work. Handoffs let agents route subtasks to the right model. If you've already built an MCP + tools workflow, handoffs let you keep a fast "coordinator" model most of the time and route to stronger models only when needed. ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio from dedalus_labs import AsyncDedalus, DedalusRunner from dotenv import load_dotenv load_dotenv() async def main(): client = AsyncDedalus() runner = DedalusRunner(client) result = await runner.run( input=( "Find me the nearest basketball games in January in San Francisco, then write a concise plan for attending." ), model=["openai/gpt-5.2", "anthropic/claude-opus-4-5"], mcp_servers=["tsion/exa"], # Web search via Exa ) print(result.final_output) if __name__ == "__main__": asyncio.run(main()) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from 'dedalus-labs'; import { DedalusRunner } from 'dedalus-labs'; const client = new Dedalus(); const runner = new DedalusRunner(client); async function main() { const result = await runner.run({ input: 'Find me the nearest basketball games in January in San Francisco, then write a concise plan for attending.', model: ['openai/gpt-5.2', 'anthropic/claude-opus-4-5'], mcpServers: ['tsion/exa'], // Web search via Exa }); console.log(result.finalOutput); } main(); ``` ## When to Use Handoffs Handoffs shine when a task has distinct phases requiring different capabilities: * **Research -> Writing**: GPT gathers information, Claude writes the final piece * **Analysis -> Code**: A reasoning model plans the approach, a code model implements it * **Triage -> Specialist**: A general model routes to domain-specific models For simple tasks where one model handles everything, stick to a single model. ## Model Strengths A rough guide to model selection: | Task | Good Models | | ----------------------- | ------------------------------------------------- | | Tool calling, reasoning | `openai/gpt-5.2`, `xai/grok-4-1-fast-reasoning` | | Writing, creative work | `anthropic/claude-opus-4-5` | | Code generation | `anthropic/claude-opus-4-5`, `openai/gpt-5-codex` | | Fast, cheap responses | `openai/gpt-5-mini` | ## Next steps * **Add multimodality**: [Images & Vision](/sdk/agents/images) -- Add image generation/vision to your workflow * **See workflows**: [Use Cases](/sdk/agents/use-cases/data-analyst) -- Multi-capability patterns [Connect these docs programmatically](/ai-optimizations/using-mintlify-mcp) to Claude, VSCode, and more via MCP for real-time answers. # Image Source: https://docs.dedaluslabs.ai/sdk/agents/image Generate, edit, and analyze image content Generate images with DALL-E, create variations, apply edits, and analyze images with vision models. All through the same unified client. For image generation, use `openai/dall-e-3` for best quality. For vision tasks, `openai/gpt-5.2` provides excellent performance. ## Progressive example: add images to your workflow If you’ve already built a text-based agent (Chat → Tools → MCP → Streaming), images are usually the next capability you add: 1. **Generate** an image from a prompt 2. **Edit / vary** an existing image 3. **Analyze** an image with a vision model The sections below start with the simplest call (generation), then layer on editing and vision. ## Image Generation Generate images from text prompts using DALL-E models. ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio from dedalus_labs import AsyncDedalus from dotenv import load_dotenv load_dotenv() async def generate_image(): """Generate image from text.""" client = AsyncDedalus() response = await client.images.generate( prompt="Dedalus flying through clouds", model="openai/dall-e-3", ) print(response.data[0].url) if __name__ == "__main__": asyncio.run(generate_image()) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from "dedalus-labs"; import * as dotenv from "dotenv"; dotenv.config(); async function generateImage() { const client = new Dedalus(); const response = await client.images.generate({ prompt: "Dedalus flying through clouds", model: "openai/dall-e-3", }); console.log(response.data[0].url); } generateImage(); ``` ## Image Editing Edit existing images by providing a source image, mask, and prompt describing desired changes. ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio import httpx from dedalus_labs import AsyncDedalus from dotenv import load_dotenv load_dotenv() async def edit_image(): """Edit image (using generated image as both source and mask).""" client = AsyncDedalus() # Generate a test image (DALL·E output is valid RGBA PNG) gen_response = await client.images.generate( prompt="A white cat on a cushion", model="openai/dall-e-2", size="512x512", ) # Download generated image async with httpx.AsyncClient() as http: img_data = await http.get(gen_response.data[0].url) img_bytes = img_data.content # Use same image as both source and mask (just testing endpoint works) response = await client.images.edit( image=img_bytes, mask=img_bytes, prompt="A white cat with sunglasses", model="openai/dall-e-2", ) print(response.data[0].url) if __name__ == "__main__": asyncio.run(edit_image()) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus, { toFile } from "dedalus-labs"; import * as dotenv from "dotenv"; dotenv.config(); async function editImage() { const client = new Dedalus(); // Generate a test image (DALL·E output is valid RGBA PNG) const genResponse = await client.images.generate({ prompt: "A white cat on a cushion", model: "openai/dall-e-2", size: "512x512", }); // Download generated image const imageUrl = genResponse.data[0].url; if (!imageUrl) throw new Error("No image URL returned"); const imageResponse = await fetch(imageUrl); const imgBytes = Buffer.from(await imageResponse.arrayBuffer()); // Use same image as both source and mask (just testing endpoint works) const response = await client.images.edit({ image: await toFile(imgBytes, "source.png"), mask: await toFile(imgBytes, "mask.png"), prompt: "A white cat with sunglasses", model: "openai/dall-e-2", }); console.log(response.data[0].url); } editImage(); ``` ## Image Variations Create variations of an existing image. ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio from pathlib import Path from dedalus_labs import AsyncDedalus from dotenv import load_dotenv load_dotenv() async def create_variations(): """Create image variations.""" client = AsyncDedalus() image_path = Path("image.png") if not image_path.exists(): print("Skipped: image.png not found") return response = await client.images.create_variation( image=image_path.read_bytes(), model="openai/dall-e-2", n=2, ) for img in response.data: print(img.url) if __name__ == "__main__": asyncio.run(create_variations()) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus, { toFile } from "dedalus-labs"; import * as fs from "fs"; import * as path from "path"; import * as dotenv from "dotenv"; dotenv.config(); async function createVariations() { const client = new Dedalus(); const imagePath = path.join(process.cwd(), "image.png"); if (!fs.existsSync(imagePath)) { console.log("Skipped: image.png not found"); return; } const response = await client.images.createVariation({ image: await toFile(fs.readFileSync(imagePath), "image.png"), model: "openai/dall-e-2", n: 2, }); for (const img of response.data) { console.log(img.url); } } createVariations(); ``` ## Vision: Analyze Images from URL Use vision models to analyze and describe images from URLs. ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio from dedalus_labs import AsyncDedalus from dotenv import load_dotenv load_dotenv() async def vision_url(): """Analyze image from URL.""" client = AsyncDedalus() completion = await client.chat.completions.create( model="openai/gpt-5.2", messages=[ { "role": "user", "content": [ {"type": "text", "text": "What's in this image?"}, { "type": "image_url", "image_url": {"url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg"}, }, ], } ], ) print(completion.choices[0].message.content) if __name__ == "__main__": asyncio.run(vision_url()) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from "dedalus-labs"; import * as dotenv from "dotenv"; dotenv.config(); async function visionUrl() { const client = new Dedalus(); const completion = await client.chat.completions.create({ model: "openai/gpt-5.2", messages: [ { role: "user", content: [ { type: "text", text: "What's in this image?" }, { type: "image_url", image_url: { url: "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg", }, }, ], }, ], }); console.log(completion.choices[0].message.content); } visionUrl(); ``` ## Vision: Analyze Local Images with Base64 Analyze local images by encoding them as base64. ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio import base64 from pathlib import Path from dedalus_labs import AsyncDedalus from dotenv import load_dotenv load_dotenv() async def vision_base64(): """Analyze local image via base64.""" client = AsyncDedalus() image_path = Path("image.png") if not image_path.exists(): print("Skipped: image.png not found") return b64 = base64.b64encode(image_path.read_bytes()).decode() completion = await client.chat.completions.create( model="openai/gpt-5.2", messages=[ { "role": "user", "content": [ {"type": "text", "text": "Describe this image."}, {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{b64}"}}, ], } ], ) print(completion.choices[0].message.content) if __name__ == "__main__": asyncio.run(vision_base64()) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from "dedalus-labs"; import * as fs from "fs"; import * as path from "path"; import * as dotenv from "dotenv"; dotenv.config(); async function visionBase64() { const client = new Dedalus(); const imagePath = path.join(process.cwd(), "image.png"); if (!fs.existsSync(imagePath)) { console.log("Skipped: image.png not found"); return; } const b64 = fs.readFileSync(imagePath).toString("base64"); const completion = await client.chat.completions.create({ model: "openai/gpt-5.2", messages: [ { role: "user", content: [ { type: "text", text: "Describe this image." }, { type: "image_url", image_url: { url: `data:image/jpeg;base64,${b64}` }, }, ], }, ], }); console.log(completion.choices[0].message.content); } visionBase64(); ``` ## Advanced: Image Orchestration with DedalusRunner Create complex image workflows by combining generation, editing, and vision capabilities using DedalusRunner. ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio import httpx from dedalus_labs import AsyncDedalus, DedalusRunner from dotenv import load_dotenv load_dotenv() class ImageToolSuite: """Helper that exposes image endpoints as DedalusRunner tools.""" def __init__(self, client: AsyncDedalus): self._client = client async def generate_concept_art( self, prompt: str, model: str = "openai/dall-e-3", size: str = "1024x1024", ) -> str: """Create concept art and return the hosted image URL.""" response = await self._client.images.generate( prompt=prompt, model=model, size=size, ) return response.data[0].url async def edit_concept_art( self, prompt: str, reference_url: str, mask_url: str | None = None, model: str = "openai/dall-e-2", ) -> str: """Apply edits to the referenced image URL and return a new URL.""" if not reference_url: raise ValueError("reference_url must be provided when editing an image.") async with httpx.AsyncClient() as http: base_image = await http.get(reference_url) mask_bytes = await http.get(mask_url) if mask_url else None edit_kwargs = { "image": base_image.content, "prompt": prompt, "model": model, } if mask_bytes: edit_kwargs["mask"] = mask_bytes.content response = await self._client.images.edit(**edit_kwargs) return response.data[0].url async def describe_image( self, image_url: str, question: str = "Describe this image.", model: str = "openai/gpt-5.2", ) -> str: """Run a lightweight vision pass against an existing image URL.""" completion = await self._client.chat.completions.create( model=model, messages=[ { "role": "user", "content": [ {"type": "text", "text": question}, {"type": "image_url", "image_url": {"url": image_url}}, ], } ], ) return completion.choices[0].message.content async def runner_storyboard(): """Demonstrate DedalusRunner + agent-as-tool pattern for image workflows.""" client = AsyncDedalus() runner = DedalusRunner(client, verbose=True) image_tools = ImageToolSuite(client) instructions = ( "You are a creative director. Use the provided tools to generate concept art, " "optionally refine it, and then describe the final render. Always keep the " "main conversation on a text model and rely on the tools for image work." ) result = await runner.run( instructions=instructions, input="Create a retro Dedalus mission patch, refine it with a neon palette, and describe it.", model="openai/gpt-5.2", tools=[ image_tools.generate_concept_art, image_tools.edit_concept_art, image_tools.describe_image, ], max_steps=4, verbose=True, debug=False, ) print("Runner final output:", result.final_output) print("Tools invoked:", result.tools_called) if __name__ == "__main__": asyncio.run(runner_storyboard()) ``` ## Next steps Multimodal patterns Transcribe and generate speech Serve your agent in production # Images & Vision Source: https://docs.dedaluslabs.ai/sdk/agents/images Generate, edit, and analyze images Generate images with DALL-E, create variations, apply edits, and analyze images with vision models. All through the same unified client. For image generation, use `openai/dall-e-3` for best quality. For vision tasks, `openai/gpt-5.2` provides excellent performance. ## Progressive example: add images to your workflow If you’ve already built a text-based agent (Chat → Tools → MCP → Streaming), images are usually the next capability you add: 1. **Generate** an image from a prompt 2. **Edit / vary** an existing image 3. **Analyze** an image with a vision model The sections below start with the simplest call (generation), then layer on editing and vision. ## Image Generation Generate images from text prompts using DALL-E models. ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio from dedalus_labs import AsyncDedalus from dotenv import load_dotenv load_dotenv() async def generate_image(): """Generate image from text.""" client = AsyncDedalus() response = await client.images.generate( prompt="Dedalus flying through clouds", model="openai/dall-e-3", ) print(response.data[0].url) if __name__ == "__main__": asyncio.run(generate_image()) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from "dedalus-labs"; import * as dotenv from "dotenv"; dotenv.config(); async function generateImage() { const client = new Dedalus(); const response = await client.images.generate({ prompt: "Dedalus flying through clouds", model: "openai/dall-e-3", }); console.log(response.data[0].url); } generateImage(); ``` ## Image Editing Edit existing images by providing a source image, mask, and prompt describing desired changes. ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio import httpx from dedalus_labs import AsyncDedalus from dotenv import load_dotenv load_dotenv() async def edit_image(): """Edit image (using generated image as both source and mask).""" client = AsyncDedalus() # Generate a test image (DALL·E output is valid RGBA PNG) gen_response = await client.images.generate( prompt="A white cat on a cushion", model="openai/dall-e-2", size="512x512", ) # Download generated image async with httpx.AsyncClient() as http: img_data = await http.get(gen_response.data[0].url) img_bytes = img_data.content # Use same image as both source and mask (just testing endpoint works) response = await client.images.edit( image=img_bytes, mask=img_bytes, prompt="A white cat with sunglasses", model="openai/dall-e-2", ) print(response.data[0].url) if __name__ == "__main__": asyncio.run(edit_image()) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus, { toFile } from "dedalus-labs"; import * as dotenv from "dotenv"; dotenv.config(); async function editImage() { const client = new Dedalus(); // Generate a test image (DALL·E output is valid RGBA PNG) const genResponse = await client.images.generate({ prompt: "A white cat on a cushion", model: "openai/dall-e-2", size: "512x512", }); // Download generated image const imageUrl = genResponse.data[0].url; if (!imageUrl) throw new Error("No image URL returned"); const imageResponse = await fetch(imageUrl); const imgBytes = Buffer.from(await imageResponse.arrayBuffer()); // Use same image as both source and mask (just testing endpoint works) const response = await client.images.edit({ image: await toFile(imgBytes, "source.png"), mask: await toFile(imgBytes, "mask.png"), prompt: "A white cat with sunglasses", model: "openai/dall-e-2", }); console.log(response.data[0].url); } editImage(); ``` ## Image Variations Create variations of an existing image. ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio from pathlib import Path from dedalus_labs import AsyncDedalus from dotenv import load_dotenv load_dotenv() async def create_variations(): """Create image variations.""" client = AsyncDedalus() image_path = Path("image.png") if not image_path.exists(): print("Skipped: image.png not found") return response = await client.images.create_variation( image=image_path.read_bytes(), model="openai/dall-e-2", n=2, ) for img in response.data: print(img.url) if __name__ == "__main__": asyncio.run(create_variations()) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus, { toFile } from "dedalus-labs"; import * as fs from "fs"; import * as path from "path"; import * as dotenv from "dotenv"; dotenv.config(); async function createVariations() { const client = new Dedalus(); const imagePath = path.join(process.cwd(), "image.png"); if (!fs.existsSync(imagePath)) { console.log("Skipped: image.png not found"); return; } const response = await client.images.createVariation({ image: await toFile(fs.readFileSync(imagePath), "image.png"), model: "openai/dall-e-2", n: 2, }); for (const img of response.data) { console.log(img.url); } } createVariations(); ``` ## Vision: Analyze Images from URL Use vision models to analyze and describe images from URLs. ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio from dedalus_labs import AsyncDedalus from dotenv import load_dotenv load_dotenv() async def vision_url(): """Analyze image from URL.""" client = AsyncDedalus() completion = await client.chat.completions.create( model="openai/gpt-5.2", messages=[ { "role": "user", "content": [ {"type": "text", "text": "What's in this image?"}, { "type": "image_url", "image_url": {"url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg"}, }, ], } ], ) print(completion.choices[0].message.content) if __name__ == "__main__": asyncio.run(vision_url()) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from "dedalus-labs"; import * as dotenv from "dotenv"; dotenv.config(); async function visionUrl() { const client = new Dedalus(); const completion = await client.chat.completions.create({ model: "openai/gpt-5.2", messages: [ { role: "user", content: [ { type: "text", text: "What's in this image?" }, { type: "image_url", image_url: { url: "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg", }, }, ], }, ], }); console.log(completion.choices[0].message.content); } visionUrl(); ``` ## Vision: Analyze Local Images with Base64 Analyze local images by encoding them as base64. ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio import base64 from pathlib import Path from dedalus_labs import AsyncDedalus from dotenv import load_dotenv load_dotenv() async def vision_base64(): """Analyze local image via base64.""" client = AsyncDedalus() image_path = Path("image.png") if not image_path.exists(): print("Skipped: image.png not found") return b64 = base64.b64encode(image_path.read_bytes()).decode() completion = await client.chat.completions.create( model="openai/gpt-5.2", messages=[ { "role": "user", "content": [ {"type": "text", "text": "Describe this image."}, {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{b64}"}}, ], } ], ) print(completion.choices[0].message.content) if __name__ == "__main__": asyncio.run(vision_base64()) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from "dedalus-labs"; import * as fs from "fs"; import * as path from "path"; import * as dotenv from "dotenv"; dotenv.config(); async function visionBase64() { const client = new Dedalus(); const imagePath = path.join(process.cwd(), "image.png"); if (!fs.existsSync(imagePath)) { console.log("Skipped: image.png not found"); return; } const b64 = fs.readFileSync(imagePath).toString("base64"); const completion = await client.chat.completions.create({ model: "openai/gpt-5.2", messages: [ { role: "user", content: [ { type: "text", text: "Describe this image." }, { type: "image_url", image_url: { url: `data:image/jpeg;base64,${b64}` } }, ], }, ], }); console.log(completion.choices[0].message.content); } visionBase64(); ``` ## Advanced: Image Orchestration with DedalusRunner Create complex image workflows by combining generation, editing, and vision capabilities using DedalusRunner. ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio import httpx from dedalus_labs import AsyncDedalus, DedalusRunner from dotenv import load_dotenv load_dotenv() class ImageToolSuite: """Helper that exposes image endpoints as DedalusRunner tools.""" def __init__(self, client: AsyncDedalus): self._client = client async def generate_concept_art( self, prompt: str, model: str = "openai/dall-e-3", size: str = "1024x1024", ) -> str: """Create concept art and return the hosted image URL.""" response = await self._client.images.generate( prompt=prompt, model=model, size=size, ) return response.data[0].url async def edit_concept_art( self, prompt: str, reference_url: str, mask_url: str | None = None, model: str = "openai/dall-e-2", ) -> str: """Apply edits to the referenced image URL and return a new URL.""" if not reference_url: raise ValueError("reference_url must be provided when editing an image.") async with httpx.AsyncClient() as http: base_image = await http.get(reference_url) mask_bytes = await http.get(mask_url) if mask_url else None edit_kwargs = { "image": base_image.content, "prompt": prompt, "model": model, } if mask_bytes: edit_kwargs["mask"] = mask_bytes.content response = await self._client.images.edit(**edit_kwargs) return response.data[0].url async def describe_image( self, image_url: str, question: str = "Describe this image.", model: str = "openai/gpt-5.2", ) -> str: """Run a lightweight vision pass against an existing image URL.""" completion = await self._client.chat.completions.create( model=model, messages=[ { "role": "user", "content": [ {"type": "text", "text": question}, {"type": "image_url", "image_url": {"url": image_url}}, ], } ], ) return completion.choices[0].message.content async def runner_storyboard(): """Demonstrate DedalusRunner + agent-as-tool pattern for image workflows.""" client = AsyncDedalus() runner = DedalusRunner(client, verbose=True) image_tools = ImageToolSuite(client) instructions = ( "You are a creative director. Use the provided tools to generate concept art, " "optionally refine it, and then describe the final render. Always keep the " "main conversation on a text model and rely on the tools for image work." ) result = await runner.run( instructions=instructions, input="Create a retro Dedalus mission patch, refine it with a neon palette, and describe it.", model="openai/gpt-5.2", tools=[ image_tools.generate_concept_art, image_tools.edit_concept_art, image_tools.describe_image, ], max_steps=4, verbose=True, debug=False, ) print("Runner final output:", result.final_output) print("Tools invoked:", result.tools_called) if __name__ == "__main__": asyncio.run(runner_storyboard()) ``` ## Next steps * **See end-to-end agents**: [Use Cases](/sdk/agents/use-cases/data-analyst) — Multimodal patterns * **Add streaming**: [Streaming](/sdk/agents/streaming) — Stream tokens as they arrive * **Build a chat server**: [Cookbook: Chat server](/cookbook) — Serve your agent in production [Connect these docs programmatically](/ai-optimizations/using-mintlify-mcp) to Claude, VSCode, and more via MCP for real-time answers. # Quickstart Source: https://docs.dedaluslabs.ai/sdk/agents/index Learn how to build, run, and deploy agents with the Dedalus SDK in minutes Dedalus helps you ship agent workflows that are: * **Provider-agnostic**: Use OpenAI, Anthropic, Google, xAI, DeepSeek, and more with one API. * **Tool- and MCP-native**: Let models call local functions and hosted MCP servers. * **Production-ready**: Streaming, structured outputs, routing/handoffs, and runtime policies. ## What are you trying to build? Send a prompt and get a response from any provider/model. Let the model call typed Python/TS functions that you implement. Print responses as they're generated (great for UIs/CLIs). Validate model output against schemas (Pydantic/Zod). Provide multiple models; the agent can route/handoff by phase. DALL-E generation, edits, variations, and vision analysis. ## Installation ```bash Python theme={"theme":{"light":"github-light","dark":"github-dark"}} uv pip install dedalus-labs ``` ```bash npm theme={"theme":{"light":"github-light","dark":"github-dark"}} npm install dedalus-labs ``` ```bash yarn theme={"theme":{"light":"github-light","dark":"github-dark"}} yarn add dedalus-labs ``` ```bash pnpm theme={"theme":{"light":"github-light","dark":"github-dark"}} pnpm add dedalus-labs ``` ```bash bun theme={"theme":{"light":"github-light","dark":"github-dark"}} bun add dedalus-labs ``` ## Set Your API Key Get your API key from the [dashboard](https://www.dedaluslabs.ai/dashboard/api-keys) and set it as an environment variable: ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} export DEDALUS_API_KEY="your-api-key" ``` Or use a `.env` file: ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} DEDALUS_API_KEY=your-api-key ``` ## Your First Request Let's build this incrementally. ### 1) Chat with a model ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio from dedalus_labs import AsyncDedalus, DedalusRunner from dotenv import load_dotenv load_dotenv() async def main(): client = AsyncDedalus() runner = DedalusRunner(client) response = await runner.run( input="What are the key factors that influence weather patterns?", model="openai/gpt-5.4", ) print(response.final_output) if __name__ == "__main__": asyncio.run(main()) ``` ### 2) Add an MCP server Here we connect a well-known MCP server and let the model use it. ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio from dedalus_labs import AsyncDedalus, DedalusRunner from dotenv import load_dotenv load_dotenv() async def main(): client = AsyncDedalus() runner = DedalusRunner(client) response = await runner.run( input="What's the weather forecast for San Francisco this week?", model="openai/gpt-5.4", mcp_servers=["windsornguyen/open-meteo-mcp"], # Weather forecasts via Open-Meteo ) print(response.final_output) if __name__ == "__main__": asyncio.run(main()) ``` ### 3) Add a local tool Define a function with type hints and a docstring. Pass it to `runner.run()`. The SDK extracts the schema automatically and handles execution when the model decides to use it. ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio from dedalus_labs import AsyncDedalus, DedalusRunner from dotenv import load_dotenv load_dotenv() def as_bullets(items: list[str]) -> str: """Format items as a bulleted list.""" return "\n".join(f"• {item}" for item in items) async def main(): client = AsyncDedalus() runner = DedalusRunner(client) response = await runner.run( input=( "Get the 7-day weather forecast for San Francisco " "and format the daily conditions as bullets using as_bullets." ), model="openai/gpt-5.4", mcp_servers=["windsornguyen/open-meteo-mcp"], tools=[as_bullets], ) print(response.final_output) if __name__ == "__main__": asyncio.run(main()) ``` ### 4) Stream output ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio from dedalus_labs import AsyncDedalus, DedalusRunner from dedalus_labs.utils.stream import stream_async from dotenv import load_dotenv load_dotenv() async def main(): client = AsyncDedalus() runner = DedalusRunner(client) stream = runner.run( input="Explain how weather forecasting works in one paragraph, streaming as you write.", model="openai/gpt-5.4", stream=True, ) await stream_async(stream) if __name__ == "__main__": asyncio.run(main()) ``` ## Next steps Start from common agent patterns and templates. End-to-end implementations and working recipes. **Go deeper**: [Tools](/sdk/agents/tools) · [MCP Servers](/sdk/agents/mcp) · [Structured Outputs](/sdk/agents/structured-outputs) · [Streaming](/sdk/agents/streaming) ## Get the latest SDKs dedalus-labs/dedalus-sdk-python dedalus-labs/dedalus-sdk-typescript [Connect these docs programmatically](/ai-optimizations/using-mintlify-mcp) to Claude, VSCode, and more via MCP for real-time answers. # MCP Servers Source: https://docs.dedaluslabs.ai/sdk/agents/mcp Connect to any model to any MCP server The Dedalus SDK is a full MCP client. Connect your agents to any server that implements the [Model Context Protocol](https://modelcontextprotocol.io), hosted by you, us, or anyone else. With `DedalusRunner`, you can mix and chain tool calls across local and remote MCP servers in one agent loop. Local tools handle your custom logic, while MCP servers add hosted capabilities (search, databases, SaaS APIs, etc.). ## Connect MCP server in one line ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio from dedalus_labs import AsyncDedalus, DedalusRunner from dotenv import load_dotenv load_dotenv() async def main(): client = AsyncDedalus() runner = DedalusRunner(client) result = await runner.run( input="What's the weather forecast for San Francisco this week?", model="anthropic/claude-opus-4-5", mcp_servers=["windsornguyen/open-meteo-mcp"], # Weather forecasts via Open-Meteo ) print(result.final_output) if __name__ == "__main__": asyncio.run(main()) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from "dedalus-labs"; import { DedalusRunner } from "dedalus-labs"; const client = new Dedalus(); const runner = new DedalusRunner(client); async function main() { const result = await runner.run({ input: "What's the weather forecast for San Francisco this week?", model: "anthropic/claude-opus-4-5", mcpServers: ["windsornguyen/open-meteo-mcp"], // Weather forecasts via Open-Meteo }); console.log(result.finalOutput); } main(); ``` The agent discovers the server's tools and uses them when relevant. ## Combine with local tools MCP servers and local tools work together. Pass both to `runner.run()`. ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio from dedalus_labs import AsyncDedalus, DedalusRunner from dotenv import load_dotenv load_dotenv() def as_bullets(items: list[str]) -> str: """Format items as a bulleted list.""" return "\n".join(f"• {item}" for item in items) async def main(): client = AsyncDedalus() runner = DedalusRunner(client) result = await runner.run( input=( "Get the 7-day weather forecast for San Francisco " "and format the daily conditions as bullets using as_bullets." ), model="anthropic/claude-opus-4-5", mcp_servers=["windsornguyen/open-meteo-mcp"], tools=[as_bullets], ) print(result.final_output) if __name__ == "__main__": asyncio.run(main()) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from "dedalus-labs"; import { DedalusRunner } from "dedalus-labs"; const client = new Dedalus(); const runner = new DedalusRunner(client); function asBullets(items: string[]): string { return items.map((item) => `• ${item}`).join("\n"); } async function main() { const result = await runner.run({ input: "Get the 7-day weather forecast for San Francisco and format the daily conditions as bullets using asBullets.", model: "anthropic/claude-opus-4-5", mcpServers: ["windsornguyen/open-meteo-mcp"], tools: [asBullets], }); console.log((result as any).finalOutput); } main(); ``` ## External MCP URL You can connect directly to any external MCP server URL (Streamable HTTP). This is useful when: * You’re testing a server without registering it * You’re connecting to a self-hosted MCP deployment * You’re using an MCP server that isn’t in the marketplace ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio from dedalus_labs import AsyncDedalus, DedalusRunner from dotenv import load_dotenv load_dotenv() async def main(): client = AsyncDedalus() runner = DedalusRunner(client) result = await runner.run( input="Use your tools to summarize the Dedalus Python SDK repo in 5 bullet points.", model="openai/gpt-5.2", # External MCP URL! mcp_servers=["https://mcp.deepwiki.com/mcp"], ) print(result.final_output) if __name__ == "__main__": asyncio.run(main()) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from "dedalus-labs"; import { DedalusRunner } from "dedalus-labs"; const client = new Dedalus(); const runner = new DedalusRunner(client); async function main() { const result = await runner.run({ input: "Use your tools to summarize the Dedalus Python SDK repo in 5 bullet points.", model: "openai/gpt-5.2", // External MCP URL! mcpServers: ["https://mcp.deepwiki.com/mcp"], }); console.log((result as any).finalOutput); } main(); ``` ## Next steps Validate and parse JSON into schemas Watch tool use + output in real time End-to-end MCP agent patterns # Models Source: https://docs.dedaluslabs.ai/sdk/agents/models One SDK, every model Dedalus is model-agnostic. Every model from every provider is accessible through a single, unified API. Switch providers by changing a string. ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_labs import Dedalus client = Dedalus() response = client.chat.completions.create( model="openai/gpt-4.1", messages=[{"role": "user", "content": "Hello!"}], ) print(response.choices[0].message.content) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from "dedalus-labs"; const client = new Dedalus(); const response = await client.chat.completions.create({ model: "openai/gpt-4.1", messages: [{ role: "user", content: "Hello!" }], }); console.log(response.choices[0].message.content); ``` Swap `openai/gpt-4.1` for `anthropic/claude-sonnet-4` or `google/gemini-2.5-pro` and everything else stays the same. ## Listing available models ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} models = client.models.list() for model in models.data: print(model.id, model.provider) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} const models = await client.models.list(); for (const model of models.data) { console.log(model.id, model.provider); } ``` Each model object includes `capabilities` (tools, vision, streaming, structured output, thinking) and `provider_info` (status, upstream API). See [Response Schemas](/api-reference/schemas#models) for the full `ListModelsResponse` shape. ## Provider format Models use the format `provider/model-name`: | Provider | Example | | --------- | ------------------------------ | | OpenAI | `openai/gpt-4.1` | | Anthropic | `anthropic/claude-sonnet-4` | | Google | `google/gemini-2.5-pro` | | xAI | `xai/grok-3` | | DeepSeek | `deepseek/deepseek-r1` | | Mistral | `mistral/mistral-large-latest` | ## Read more Install the SDK and make your first call Bring your own provider API keys Give models the ability to call functions Stream responses token by token # Policies Source: https://docs.dedaluslabs.ai/sdk/agents/policies Control agent behavior dynamically at runtime Policies let you inject logic at each step of agent execution. Add instructions, modify behavior, enforce constraints—all based on runtime context like step count, previous outputs, or external state. ## Basic Policy A policy is a function that receives context and returns modifications: ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio from dedalus_labs import AsyncDedalus, DedalusRunner from dedalus_labs.utils.stream import stream_async from dotenv import load_dotenv load_dotenv() def policy(ctx: dict) -> dict: step = ctx.get("step", 1) if step >= 3: # After step 3, tell the model to wrap up return { "message_prepend": [ {"role": "system", "content": "Provide your final answer now."} ], "max_steps": 4 } return {} async def main(): client = AsyncDedalus() runner = DedalusRunner(client) result = runner.run( input="Research the history of the internet and summarize key milestones", model="openai/gpt-5.2", mcp_servers=["windsor/brave-search-mcp"], # Web search via Brave Search MCP stream=True, policy=policy ) await stream_async(result) if __name__ == "__main__": asyncio.run(main()) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from "dedalus-labs"; import { DedalusRunner } from "dedalus-labs"; const client = new Dedalus(); const runner = new DedalusRunner(client, true); function policy(ctx: { step?: number }): object { const step = ctx.step ?? 1; if (step >= 3) { return { messagePrepend: [{ role: "system", content: "Provide your final answer now." }], maxSteps: 4, }; } return {}; } async function main() { const result = await runner.run({ input: "Research the history of the internet and summarize key milestones", model: "openai/gpt-5.2", mcpServers: ["windsor/brave-search-mcp"], // Web search via Brave Search MCP stream: true, policy: policy, }); if (Symbol.asyncIterator in result) { for await (const chunk of result) { if (chunk.choices?.[0]?.delta?.content) { process.stdout.write(chunk.choices[0].delta.content); } } } } main(); ``` ## Policy Context The `ctx` dict contains: | Field | Type | Description | | -------------- | ---- | ---------------------------------- | | `step` | int | Current execution step (1-indexed) | | `messages` | list | Conversation history so far | | `tools_called` | list | Tools invoked in previous steps | ## Policy Returns Policies can return: | Field | Effect | | ------------------------------------ | ----------------------------------------- | | `message_prepend` / `messagePrepend` | Messages added before the next model call | | `message_append` / `messageAppend` | Messages added after the conversation | | `max_steps` / `maxSteps` | Override the maximum step count | | `stop` | Boolean to halt execution early | ## Use Cases **Rate limiting**: Track API calls across steps, pause if limits approached. **Guardrails**: Check outputs for policy violations, inject correction prompts. **Dynamic instructions**: Change behavior based on intermediate results. **Cost control**: Stop execution after a certain number of expensive operations. ## Tool Event Callbacks Monitor tool execution with `on_tool_event`: ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} import json def on_tool(evt: dict) -> None: print(f"Tool called: {json.dumps(evt)}") result = runner.run( input="Calculate shipping costs for a 5kg package to London", model="openai/gpt-5.2", tools=[calculate_shipping], on_tool_event=on_tool, policy=policy ) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} function onTool(evt: object): void { console.log("Tool called:", JSON.stringify(evt)); } const result = await runner.run({ input: "Calculate shipping costs for a 5kg package to London", model: "openai/gpt-5.2", tools: [calculateShipping], onToolEvent: onTool, policy: policy, }); ``` ## Next steps Define the tools policies can control # Python SDK Source: https://docs.dedaluslabs.ai/sdk/agents/python Agents API Python SDK v0.3.0 **v0.3.0** | [GitHub](https://github.com/dedalus-labs/dedalus-sdk-python) | [Changelog](https://github.com/dedalus-labs/dedalus-sdk-python/blob/main/CHANGELOG.md) ## Installation ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} pip install dedalus-labs ``` ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} uv add dedalus-labs ``` ## Usage See the full method reference in the [API Reference](/api-reference/dcs) tab. ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} import os from dedalus_labs import Dedalus client = Dedalus( api_key=os.environ.get("DEDALUS_API_KEY"), # This is the default and can be omitted # defaults to "production". environment="development", ) chat_completion = client.chat.completions.create( model="openai/gpt-5-nano", messages=[ { "role": "system", "content": "You are Stephen Dedalus. Respond in morose Joycean malaise.", }, { "role": "user", "content": "Hello, how are you today?", }, ], ) print(chat_completion.id) ``` While you can provide an `api_key` keyword argument, we recommend using [python-dotenv](https://pypi.org/project/python-dotenv/) to add `DEDALUS_API_KEY="My API Key"` to your `.env` file so that your API Key is not stored in source control. ## Async Usage Simply import `AsyncDedalus` instead of `Dedalus` and use `await` with each API call: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} import os import asyncio from dedalus_labs import AsyncDedalus client = AsyncDedalus( api_key=os.environ.get("DEDALUS_API_KEY"), # This is the default and can be omitted # defaults to "production". environment="development", ) async def main() -> None: chat_completion = await client.chat.completions.create( model="openai/gpt-5-nano", messages=[ { "role": "system", "content": "You are Stephen Dedalus. Respond in morose Joycean malaise.", }, { "role": "user", "content": "Hello, how are you today?", }, ], ) print(chat_completion.id) asyncio.run(main()) ``` Functionality between the synchronous and asynchronous clients is otherwise identical. ### With aiohttp By default, the async client uses `httpx` for HTTP requests. However, for improved concurrency performance you may also use `aiohttp` as the HTTP backend. You can enable this by installing `aiohttp`: ```sh theme={"theme":{"light":"github-light","dark":"github-dark"}} # install from PyPI pip install dedalus_labs[aiohttp] ``` Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} import os import asyncio from dedalus_labs import DefaultAioHttpClient from dedalus_labs import AsyncDedalus async def main() -> None: async with AsyncDedalus( api_key=os.environ.get("DEDALUS_API_KEY"), # This is the default and can be omitted http_client=DefaultAioHttpClient(), ) as client: chat_completion = await client.chat.completions.create( model="openai/gpt-5-nano", messages=[ { "role": "system", "content": "You are Stephen Dedalus. Respond in morose Joycean malaise.", }, { "role": "user", "content": "Hello, how are you today?", }, ], ) print(chat_completion.id) asyncio.run(main()) ``` ## Streaming We provide support for streaming responses using Server Side Events (SSE). ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_labs import Dedalus client = Dedalus() stream = client.chat.completions.create( model="openai/gpt-5-nano", stream=True, messages=[ { "role": "system", "content": "You are Stephen Dedalus. Respond in morose Joycean malaise.", }, { "role": "user", "content": "What do you think of artificial intelligence?", }, ], ) for chat_completion in stream: print(chat_completion.id) ``` The async client uses the exact same interface. ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_labs import AsyncDedalus client = AsyncDedalus() stream = await client.chat.completions.create( model="openai/gpt-5-nano", stream=True, messages=[ { "role": "system", "content": "You are Stephen Dedalus. Respond in morose Joycean malaise.", }, { "role": "user", "content": "What do you think of artificial intelligence?", }, ], ) async for chat_completion in stream: print(chat_completion.id) ``` ## Using types Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typing.html#typing.TypedDict). Responses are [Pydantic models](https://docs.pydantic.dev) which also provide helper methods for things like: * Serializing back into JSON, `model.to_json()` * Converting to a dictionary, `model.to_dict()` Typed requests and responses provide autocomplete and documentation within your editor. If you would like to see type errors in VS Code to help catch bugs earlier, set `python.analysis.typeCheckingMode` to `basic`. ## Nested params Nested parameters are dictionaries, typed using `TypedDict`, for example: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_labs import Dedalus client = Dedalus() chat_completion = client.chat.completions.create( model="openai/gpt-5", audio={ "format": "wav", "voice": "string", }, ) print(chat_completion.audio) ``` ## File uploads Request parameters that correspond to file uploads can be passed as `bytes`, or a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance or a tuple of `(filename, contents, media type)`. ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from pathlib import Path from dedalus_labs import Dedalus client = Dedalus() client.audio.transcriptions.create( file=Path("/path/to/file"), model="model", ) ``` The async client uses the exact same interface. If you pass a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance, the file contents will be read asynchronously automatically. ## Error Handling Always wrap API calls in try/catch. The SDK throws typed errors for HTTP failures. When the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `dedalus_labs.APIConnectionError` is raised. When the API returns a non-success status code (that is, 4xx or 5xx response), a subclass of `dedalus_labs.APIStatusError` is raised, containing `status_code` and `response` properties. All errors inherit from `dedalus_labs.APIError`. ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} import dedalus_labs from dedalus_labs import Dedalus client = Dedalus() try: client.chat.completions.create( model="openai/gpt-5-nano", messages=[ { "role": "system", "content": "You are Stephen Dedalus. Respond in morose Joycean malaise.", }, { "role": "user", "content": "Hello, how are you today?", }, ], ) except dedalus_labs.APIConnectionError as e: print("The server could not be reached") print(e.__cause__) # an underlying Exception, likely raised within httpx. except dedalus_labs.RateLimitError as e: print("A 429 status code was received; we should back off a bit.") except dedalus_labs.APIStatusError as e: print("Another non-200-range status code was received") print(e.status_code) print(e.response) ``` Error codes are as follows: | Status Code | Error Type | | ----------- | -------------------------- | | 400 | `BadRequestError` | | 401 | `AuthenticationError` | | 403 | `PermissionDeniedError` | | 404 | `NotFoundError` | | 422 | `UnprocessableEntityError` | | 429 | `RateLimitError` | | >=500 | `InternalServerError` | | N/A | `APIConnectionError` | ### Retries Certain errors are automatically retried 2 times by default, with a short exponential backoff. Connection errors (for example, due to a network connectivity problem), 408 Request Timeout, 409 Conflict, 429 Rate Limit, and >=500 Internal errors are all retried by default. You can use the `max_retries` option to configure or disable retry settings: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_labs import Dedalus # Configure the default for all requests: client = Dedalus( # default is 2 max_retries=0, ) # Or, configure per-request: client.with_options(max_retries=5).chat.completions.create( model="openai/gpt-5-nano", messages=[ { "role": "system", "content": "You are Stephen Dedalus. Respond in morose Joycean malaise.", }, { "role": "user", "content": "Hello, how are you today?", }, ], ) ``` ### Timeouts By default requests time out after 1 minute. You can configure this with a `timeout` option, which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/timeouts/#fine-tuning-the-configuration) object: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_labs import Dedalus # Configure the default for all requests: client = Dedalus( # 20 seconds (default is 1 minute) timeout=20.0, ) # More granular control: client = Dedalus( timeout=httpx.Timeout(60.0, read=5.0, write=10.0, connect=2.0), ) # Override per-request: client.with_options(timeout=5.0).chat.completions.create( model="openai/gpt-5-nano", messages=[ { "role": "system", "content": "You are Stephen Dedalus. Respond in morose Joycean malaise.", }, { "role": "user", "content": "Hello, how are you today?", }, ], ) ``` On timeout, an `APITimeoutError` is thrown. Note that requests that time out are [retried twice by default](#retries). ## Default Headers We automatically send the following headers with all requests. | Header | Value | | --------------- | ------------- | | `User-Agent` | `Dedalus-SDK` | | `X-SDK-Version` | `1.0.0` | If you need to, you can override these headers by setting default headers per-request or on the client object. ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_labs import Dedalus client = Dedalus( default_headers={"User-Agent": "My-Custom-Value"}, ) ``` ### Logging We use the standard library [`logging`](https://docs.python.org/3/library/logging.html) module. You can enable logging by setting the environment variable `DEDALUS_LOG` to `info`. ```shell theme={"theme":{"light":"github-light","dark":"github-dark"}} $ export DEDALUS_LOG=info ``` Or to `debug` for more verbose logging. ### How to tell whether `None` means `null` or missing In an API response, a field may be explicitly `null`, or missing entirely; in either case, its value is `None` in this library. You can differentiate the two cases with `.model_fields_set`: ```py theme={"theme":{"light":"github-light","dark":"github-dark"}} if response.my_field is None: if 'my_field' not in response.model_fields_set: print('Got json like {}, without a "my_field" key present at all.') else: print('Got json like {"my_field": null}.') ``` ### Accessing raw response data (e.g. headers) The "raw" Response object can be accessed by prefixing `.with_raw_response.` to any HTTP method call, e.g., ```py theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_labs import Dedalus client = Dedalus() response = client.chat.completions.with_raw_response.create( model="openai/gpt-5-nano", messages=[{ "role": "system", "content": "You are Stephen Dedalus. Respond in morose Joycean malaise.", }, { "role": "user", "content": "Hello, how are you today?", }], ) print(response.headers.get('X-My-Header')) completion = response.parse() # get the object that `chat.completions.create()` would have returned print(completion.id) ``` These methods return an [`APIResponse`](https://github.com/dedalus-labs/dedalus-sdk-python/tree/main/src/dedalus_labs/_response.py) object. The async client returns an [`AsyncAPIResponse`](https://github.com/dedalus-labs/dedalus-sdk-python/tree/main/src/dedalus_labs/_response.py) with the same structure, the only difference being `await`able methods for reading the response content. #### `.with_streaming_response` The above interface eagerly reads the full response body when you make the request, which may not always be what you want. To stream the response body, use `.with_streaming_response` instead, which requires a context manager and only reads the response body once you call `.read()`, `.text()`, `.json()`, `.iter_bytes()`, `.iter_text()`, `.iter_lines()` or `.parse()`. In the async client, these are async methods. ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} with client.chat.completions.with_streaming_response.create( model="openai/gpt-5-nano", messages=[ { "role": "system", "content": "You are Stephen Dedalus. Respond in morose Joycean malaise.", }, { "role": "user", "content": "Hello, how are you today?", }, ], ) as response: print(response.headers.get("X-My-Header")) for line in response.iter_lines(): print(line) ``` The context manager is required so that the response will reliably be closed. ### Making custom/undocumented requests This library is typed for convenient access to the documented API. If you need to access undocumented endpoints, params, or response properties, the library can still be used. #### Undocumented endpoints To make requests to undocumented endpoints, you can make requests using `client.get`, `client.post`, and other http verbs. Options on the client will be respected (such as retries) when making this request. ```py theme={"theme":{"light":"github-light","dark":"github-dark"}} import httpx response = client.post( "/foo", cast_to=httpx.Response, body={"my_param": True}, ) print(response.headers.get("x-foo")) ``` #### Undocumented request params If you want to explicitly send an extra param, you can do so with the `extra_query`, `extra_body`, and `extra_headers` request options. #### Undocumented response properties To access undocumented response properties, you can access the extra fields like `response.unknown_prop`. You can also get all the extra fields on the Pydantic model as a dict with [`response.model_extra`](https://docs.pydantic.dev/latest/api/base_model/). ### Configuring the HTTP client You can directly override the [httpx client](https://www.python-httpx.org/api/#client) to customize it for your use case, including: * Support for [proxies](https://www.python-httpx.org/advanced/proxies/) * Custom [transports](https://www.python-httpx.org/advanced/transports/) * Additional [advanced](https://www.python-httpx.org/advanced/clients/) functionality ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} import httpx from dedalus_labs import Dedalus, DefaultHttpxClient client = Dedalus( # Or use the `DEDALUS_BASE_URL` env var base_url="http://my.test.server.example.com:8083", http_client=DefaultHttpxClient( proxy="http://my.test.proxy.example.com", transport=httpx.HTTPTransport(local_address="0.0.0.0"), ), ) ``` You can also customize the client on a per-request basis by using `with_options()`: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} client.with_options(http_client=DefaultHttpxClient(...)) ``` ### Managing HTTP resources By default the library closes underlying HTTP connections whenever the client is [garbage collected](https://docs.python.org/3/reference/datamodel.html#object.__del__). You can manually close the client using the `.close()` method if desired, or with a context manager that closes when exiting. ```py theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_labs import Dedalus with Dedalus() as client: # make requests here ... # HTTP client is now closed ``` ## Versioning This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions: 1. Changes that only affect static types, without breaking runtime behavior. 2. Changes to library internals which are technically public but not intended or documented for external use. *(Please open a GitHub issue to let us know if you are relying on such internals.)* 3. Changes that we do not expect to impact the vast majority of users in practice. We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience. We are keen for your feedback; please open an [issue](https://www.github.com/dedalus-labs/dedalus-sdk-python/issues) with questions, bugs, or suggestions. ### Determining the installed version If you've upgraded to the latest version but aren't seeing any new features you were expecting then your python environment is likely still using an older version. You can determine the version that is being used at runtime with: ```py theme={"theme":{"light":"github-light","dark":"github-dark"}} import dedalus_labs print(dedalus_labs.__version__) ``` Python 3.9 or higher. # Runner Reference Source: https://docs.dedaluslabs.ai/sdk/agents/runner Complete reference for DedalusRunner.run() parameters `DedalusRunner` is the core of the Dedalus SDK. It orchestrates local tools, hosted MCP servers, streaming, and any model from any provider into a single agentic loop. Five lines of code, any agent you want. ## Quick Example ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_labs import AsyncDedalus, DedalusRunner client = AsyncDedalus() runner = DedalusRunner(client) result = await runner.run( input="What's the weather in Tokyo?", model="anthropic/claude-sonnet-4-20250514", mcp_servers=["windsornguyen/open-meteo-mcp"], max_steps=5, ) print(result.final_output) ``` *** ## Parameters The user's prompt or a list of messages. Use a string for single-turn requests; use a message list for multi-turn conversations. Model(s) to use. Format: `provider/model-name` (e.g., `openai/gpt-4o`, `anthropic/claude-sonnet-4-20250514`). Pass a list for routing or fallback behavior. System prompt that defines the agent's behavior and personality. Existing conversation history. Use with `result.to_input_list()` for multi-turn conversations. Local Python/TS functions the model can call. Schema extracted automatically from type hints and docstrings. See [Tools](/sdk/agents/tools). Hosted MCP servers to connect. Format: `["owner/server-name"]`. Credentials for MCP server authentication. Control tool usage: * `"auto"` — Model decides (default) * `"none"` — Disable tools * `"required"` — Force tool use * `{"type": "function", "function": {"name": "..."}}` — Force specific tool Sampling temperature (0–2). Higher values increase randomness. Default varies by model. Maximum tokens in the response. Nucleus sampling threshold (0–1). Alternative to temperature. Penalize repeated tokens based on frequency (-2.0 to 2.0). Penalize tokens that have appeared at all (-2.0 to 2.0). Adjust likelihood of specific tokens. Maps token IDs to bias values (-100 to 100). Enforce structured output. Pass a Pydantic model or JSON schema. See [Structured Outputs](/sdk/agents/structured-outputs). Return an async iterator for streaming responses. See [Streaming](/sdk/agents/streaming). Include model's intent analysis in result. Maximum agentic loop iterations. The loop runs until the model stops calling tools or hits this limit. Transport protocol: `"http"` or `"realtime"`. Runtime policies for dynamic model selection or behavior modification. Configuration for agent-to-agent handoffs. See [Handoffs](/sdk/agents/handoffs). Attributes for agent routing and selection. Maps attribute names to float values. Per-model attribute overrides. Maps model names to attribute dictionaries. Restrict which models the agent can use. Enforce strict model validation. Input/output guardrail configurations. Enable verbose logging. Enable debug mode with detailed traces and conversation snapshots. Callback fired when tools are called. Receives tool call details as a dictionary. *** ## Return Value Response object returned by `runner.run()`. The final text response from the agent. Results from local tool executions. Each contains `name`, `result`, `step`, and optionally `error`. Results from MCP server tool calls. Names of tools that were invoked during the run. Number of agentic loop iterations used. Full conversation history including tool calls. Useful for debugging or continuing conversations. Model's intent analysis (only present if `return_intent=true`). Alias for `final_output`. Alias for `final_output`. Returns a copy of the conversation history for use in follow-up runs. Enables multi-turn conversations. ```python Multi-turn Chat theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio from dedalus_labs import AsyncDedalus, DedalusRunner async def main(): client = AsyncDedalus() runner = DedalusRunner(client) messages: list[dict] = [] while True: user_input = input("You: ").strip() if not user_input: break messages.append({"role": "user", "content": user_input}) result = await runner.run( model="openai/gpt-4o", messages=messages, ) messages = result.to_input_list() print(f"Assistant: {result.final_output}\n") asyncio.run(main()) ``` ```json Example Response theme={"theme":{"light":"github-light","dark":"github-dark"}} { "final_output": "The weather in Tokyo is currently 18°C with clear skies.", "tool_results": [], "mcp_results": [ { "name": "get_current_weather", "result": {"temperature": 18, "conditions": "clear"}, "server": "windsornguyen/open-meteo-mcp" } ], "tools_called": ["get_current_weather"], "steps_used": 2, "messages": [...] } ``` *** ## Next Steps Define local functions the model can call. Route between models mid-conversation. Validate responses against schemas. Stream responses as they generate. # Dedalus Runner Source: https://docs.dedaluslabs.ai/sdk/agents/runner/python Complete reference for DedalusRunner.run() parameters `DedalusRunner` is the core of the Dedalus SDK. It orchestrates local tools, local and remote MCP servers, streaming, and any model from any provider into a single agentic loop. Its core job is to mix and chain tool calls across those systems inside one run. Five lines of code, any agent you want. ## Quick Example ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_labs import AsyncDedalus, DedalusRunner client = AsyncDedalus() runner = DedalusRunner(client) result = await runner.run( input="What's the weather in Tokyo?", model="anthropic/claude-sonnet-4-20250514", mcp_servers=["windsornguyen/open-meteo-mcp"], max_steps=5, ) print(result.final_output) ``` *** ## Parameters The user's prompt or a list of messages. Use a string for single-turn requests; use a message list for multi-turn conversations. Model(s) to use. Format: `provider/model-name` (e.g., `openai/gpt-4o`, `anthropic/claude-sonnet-4-20250514`). Pass a list for routing or fallback behavior. System prompt that defines the agent's behavior and personality. Existing conversation history. Use with `result.to_input_list()` for multi-turn conversations. Local Python/TS functions the model can call. Schema extracted automatically from type hints and docstrings. See [Tools](/sdk/agents/tools). Hosted MCP servers to connect. Format: `["owner/server-name"]`. See [MCP](/sdk/agents/mcp). Credentials for MCP server authentication. Control tool usage: * `"auto"` — Model decides (default) * `"none"` — Disable tools * `"required"` — Force tool use * `{"type": "function", "function": {"name": "..."}}` — Force specific tool Sampling temperature (0–2). Higher values increase randomness. Default varies by model. Maximum tokens in the response. Nucleus sampling threshold (0–1). Alternative to temperature. Penalize repeated tokens based on frequency (-2.0 to 2.0). Penalize tokens that have appeared at all (-2.0 to 2.0). Adjust likelihood of specific tokens. Maps token IDs to bias values (-100 to 100). Enforce structured output. Pass a Pydantic model or JSON schema. See [Structured Outputs](/sdk/agents/structured-outputs). Return an async iterator for streaming responses. See [Streaming](/sdk/agents/streaming). Include model's intent analysis in result. Maximum agentic loop iterations. The loop runs until the model stops calling tools or hits this limit. Transport protocol: `"http"` or `"realtime"`. Runtime policies for dynamic model selection or behavior modification. Configuration for agent-to-agent handoffs. See [Handoffs](/sdk/agents/handoffs). Attributes for agent routing and selection. Maps attribute names to float values. Per-model attribute overrides. Maps model names to attribute dictionaries. Restrict which models the agent can use. Enforce strict model validation. Input/output guardrail configurations. Enable verbose logging. Enable debug mode with detailed traces and conversation snapshots. Callback fired when tools are called. Receives tool call details as a dictionary. *** ## Return Value Response object returned by `runner.run()`. The final text response from the agent. Results from local tool executions. Each contains `name`, `result`, `step`, and optionally `error`. Results from MCP server tool calls. Names of tools that were invoked during the run. Number of agentic loop iterations used. Full conversation history including tool calls. Useful for debugging or continuing conversations. Model's intent analysis (only present if `return_intent=true`). Alias for `final_output`. Alias for `final_output`. Returns a copy of the conversation history for use in follow-up runs. Enables multi-turn conversations. ```python Multi-turn Chat theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio from dedalus_labs import AsyncDedalus, DedalusRunner async def main(): client = AsyncDedalus() runner = DedalusRunner(client) messages: list[dict] = [] while True: user_input = input("You: ").strip() if not user_input: break messages.append({"role": "user", "content": user_input}) result = await runner.run( model="openai/gpt-4o", messages=messages, ) messages = result.to_input_list() print(f"Assistant: {result.final_output}\n") asyncio.run(main()) ``` ```json Example Response theme={"theme":{"light":"github-light","dark":"github-dark"}} { "final_output": "The weather in Tokyo is currently 18°C with clear skies.", "tool_results": [], "mcp_results": [ { "name": "get_current_weather", "result": {"temperature": 18, "conditions": "clear"}, "server": "windsornguyen/open-meteo-mcp" } ], "tools_called": ["get_current_weather"], "steps_used": 2, "messages": [...] } ``` *** ## Next Steps Define local functions the model can call. Connect to hosted MCP servers. Validate responses against schemas. Stream responses as they generate. # Dedalus Runner Source: https://docs.dedaluslabs.ai/sdk/agents/runner/typescript Complete reference for DedalusRunner.run() parameters `DedalusRunner` is the core of the Dedalus SDK. It orchestrates local tools, local and remote MCP servers, streaming, and any model from any provider into a single agentic loop. Its core job is to mix and chain tool calls across those systems inside one run. Five lines of code, any agent you want. ## Quick Example ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_labs import AsyncDedalus, DedalusRunner client = AsyncDedalus() runner = DedalusRunner(client) result = await runner.run( input="What's the weather in Tokyo?", model="anthropic/claude-sonnet-4-20250514", mcp_servers=["windsornguyen/open-meteo-mcp"], max_steps=5, ) print(result.final_output) ``` *** ## Parameters The user's prompt or a list of messages. Use a string for single-turn requests; use a message list for multi-turn conversations. Model(s) to use. Format: `provider/model-name` (e.g., `openai/gpt-4o`, `anthropic/claude-sonnet-4-20250514`). Pass a list for routing or fallback behavior. System prompt that defines the agent's behavior and personality. Existing conversation history. Use with `result.to_input_list()` for multi-turn conversations. Local Python/TS functions the model can call. Schema extracted automatically from type hints and docstrings. See [Tools](/sdk/agents/tools). Hosted MCP servers to connect. Format: `["owner/server-name"]`. See [MCP](/sdk/agents/mcp). Credentials for MCP server authentication. Control tool usage: * `"auto"` — Model decides (default) * `"none"` — Disable tools * `"required"` — Force tool use * `{"type": "function", "function": {"name": "..."}}` — Force specific tool Sampling temperature (0–2). Higher values increase randomness. Default varies by model. Maximum tokens in the response. Nucleus sampling threshold (0–1). Alternative to temperature. Penalize repeated tokens based on frequency (-2.0 to 2.0). Penalize tokens that have appeared at all (-2.0 to 2.0). Adjust likelihood of specific tokens. Maps token IDs to bias values (-100 to 100). Enforce structured output. Pass a Pydantic model or JSON schema. See [Structured Outputs](/sdk/agents/structured-outputs). Return an async iterator for streaming responses. See [Streaming](/sdk/agents/streaming). Include model's intent analysis in result. Maximum agentic loop iterations. The loop runs until the model stops calling tools or hits this limit. Transport protocol: `"http"` or `"realtime"`. Runtime policies for dynamic model selection or behavior modification. Configuration for agent-to-agent handoffs. See [Handoffs](/sdk/agents/handoffs). Attributes for agent routing and selection. Maps attribute names to float values. Per-model attribute overrides. Maps model names to attribute dictionaries. Restrict which models the agent can use. Enforce strict model validation. Input/output guardrail configurations. Enable verbose logging. Enable debug mode with detailed traces and conversation snapshots. Callback fired when tools are called. Receives tool call details as a dictionary. *** ## Return Value Response object returned by `runner.run()`. The final text response from the agent. Results from local tool executions. Each contains `name`, `result`, `step`, and optionally `error`. Results from MCP server tool calls. Names of tools that were invoked during the run. Number of agentic loop iterations used. Full conversation history including tool calls. Useful for debugging or continuing conversations. Model's intent analysis (only present if `return_intent=true`). Alias for `final_output`. Alias for `final_output`. Returns a copy of the conversation history for use in follow-up runs. Enables multi-turn conversations. ```python Multi-turn Chat theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio from dedalus_labs import AsyncDedalus, DedalusRunner async def main(): client = AsyncDedalus() runner = DedalusRunner(client) messages: list[dict] = [] while True: user_input = input("You: ").strip() if not user_input: break messages.append({"role": "user", "content": user_input}) result = await runner.run( model="openai/gpt-4o", messages=messages, ) messages = result.to_input_list() print(f"Assistant: {result.final_output}\n") asyncio.run(main()) ``` ```json Example Response theme={"theme":{"light":"github-light","dark":"github-dark"}} { "final_output": "The weather in Tokyo is currently 18°C with clear skies.", "tool_results": [], "mcp_results": [ { "name": "get_current_weather", "result": {"temperature": 18, "conditions": "clear"}, "server": "windsornguyen/open-meteo-mcp" } ], "tools_called": ["get_current_weather"], "steps_used": 2, "messages": [...] } ``` *** ## Next Steps Define local functions the model can call. Connect to hosted MCP servers. Validate responses against schemas. Stream responses as they generate. # Streaming Source: https://docs.dedaluslabs.ai/sdk/agents/streaming Display responses as they're generated Streaming shows output token-by-token instead of waiting for the complete response. Users see progress immediately, which matters for longer outputs or interactive applications. ## Stream in one line Set `stream=True` so users see progress as the agent works. ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio from dedalus_labs import AsyncDedalus, DedalusRunner from dedalus_labs.utils.stream import stream_async from dotenv import load_dotenv load_dotenv() async def main(): client = AsyncDedalus() runner = DedalusRunner(client) stream = runner.run( input="Find me the nearest basketball games in January in San Francisco (stream your work).", model="anthropic/claude-opus-4-5", mcp_servers=["tsion/exa"], # Web search via Exa stream=True, ) await stream_async(stream) if __name__ == "__main__": asyncio.run(main()) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from "dedalus-labs"; import { DedalusRunner } from "dedalus-labs"; const client = new Dedalus(); const runner = new DedalusRunner(client, true); async function main() { const result = await runner.run({ input: "Find me the nearest basketball games in January in San Francisco (stream your work).", model: "anthropic/claude-opus-4-5", mcpServers: ["tsion/exa"], // Web search via Exa stream: true, }); if (Symbol.asyncIterator in result) { for await (const chunk of result) { if (chunk.choices?.[0]?.delta?.content) { process.stdout.write(chunk.choices[0].delta.content); } } } } main(); ``` ## Streaming with Tools Streaming works with tool-calling workflows. You can stream while the agent calls **local tools**, **MCPs**, or both. ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio from dedalus_labs import AsyncDedalus, DedalusRunner from dedalus_labs.utils.stream import stream_async from dotenv import load_dotenv load_dotenv() def summarize_headlines(headlines: list[str]) -> str: """Format headlines as a short bullet list.""" return "\n".join(f"• {h}" for h in headlines[:3]) async def main(): client = AsyncDedalus() runner = DedalusRunner(client) stream = runner.run( input=( "Search for AI news. Extract 3 headlines. " "Then call summarize_headlines(headlines) and stream your final answer." ), model="openai/gpt-5.2", mcp_servers=["windsor/brave-search-mcp"], # Web search via Brave Search MCP tools=[summarize_headlines], stream=True, ) await stream_async(stream) if __name__ == "__main__": asyncio.run(main()) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from "dedalus-labs"; import { DedalusRunner } from "dedalus-labs"; function summarizeHeadlines(headlines: string[]): string { return headlines .slice(0, 3) .map((h) => `• ${h}`) .join("\n"); } const client = new Dedalus(); const runner = new DedalusRunner(client, true); async function main() { const result = await runner.run({ input: "Search for AI news. Extract 3 headlines. Then call summarizeHeadlines(headlines) and stream your final answer.", model: "openai/gpt-5.2", mcpServers: ["windsor/brave-search-mcp"], // Web search via Brave Search MCP tools: [summarizeHeadlines], stream: true, }); if (Symbol.asyncIterator in result) { for await (const chunk of result) { if (chunk.choices?.[0]?.delta?.content) { process.stdout.write(chunk.choices[0].delta.content); } } } } main(); ``` ## Compare: non-streaming vs streaming (same scenario) The scenario below is the same in both snippets. The only difference is whether you set `stream=True` **and iterate over the stream**. In Python, **non-streaming** refers to `stream=False`, not “sync”. If you use `AsyncDedalus`, you’ll still write async code and use `asyncio.run(...)`. If you prefer fully synchronous code, use the `Dedalus` client (example below). ### Python ```python Non-streaming (AsyncDedalus) theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio from dedalus_labs import AsyncDedalus, DedalusRunner from dotenv import load_dotenv load_dotenv() async def main(): client = AsyncDedalus() runner = DedalusRunner(client) result = await runner.run( input="Find me the nearest basketball games in January in San Francisco.", model="anthropic/claude-opus-4-5", mcp_servers=["tsion/exa"], # Web search via Exa ) # You only see output after the full run completes. print(result.final_output) if __name__ == "__main__": asyncio.run(main()) ``` ```python Streaming (AsyncDedalus) theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio from dedalus_labs import AsyncDedalus, DedalusRunner from dedalus_labs.utils.stream import stream_async from dotenv import load_dotenv load_dotenv() async def main(): client = AsyncDedalus() runner = DedalusRunner(client) stream = runner.run( input="Find me the nearest basketball games in January in San Francisco.", model="anthropic/claude-opus-4-5", mcp_servers=["tsion/exa"], # Web search via Exa stream=True, ) # You see output as the model generates it. await stream_async(stream) if __name__ == "__main__": asyncio.run(main()) ``` ### Python (sync client) ```python Non-streaming (Dedalus) theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_labs import Dedalus, DedalusRunner from dotenv import load_dotenv load_dotenv() def main(): client = Dedalus() runner = DedalusRunner(client) result = runner.run( input="Find me the nearest basketball games in January in San Francisco.", model="anthropic/claude-opus-4-5", mcp_servers=["tsion/exa"], # Web search via Exa ) print(result.final_output) if __name__ == "__main__": main() ``` ```python Streaming (Dedalus) theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_labs import Dedalus, DedalusRunner from dedalus_labs.utils.stream import stream_sync from dotenv import load_dotenv load_dotenv() def main(): client = Dedalus() runner = DedalusRunner(client) stream = runner.run( input="Find me the nearest basketball games in January in San Francisco.", model="anthropic/claude-opus-4-5", mcp_servers=["tsion/exa"], # Web search via Exa stream=True, ) stream_sync(stream) if __name__ == "__main__": main() ``` ### TypeScript ```typescript Non-streaming theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from "dedalus-labs"; import { DedalusRunner } from "dedalus-labs"; const client = new Dedalus(); const runner = new DedalusRunner(client, true); async function main() { const result = await runner.run({ input: "Find me the nearest basketball games in January in San Francisco.", model: "anthropic/claude-opus-4-5", mcpServers: ["tsion/exa"], // Web search via Exa }); console.log(result.finalOutput); } main(); ``` ```typescript Streaming theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from "dedalus-labs"; import { DedalusRunner } from "dedalus-labs"; const client = new Dedalus(); const runner = new DedalusRunner(client, true); async function main() { const result = await runner.run({ input: "Find me the nearest basketball games in January in San Francisco.", model: "anthropic/claude-opus-4-5", mcpServers: ["tsion/exa"], // Web search via Exa stream: true, }); if (Symbol.asyncIterator in result) { for await (const chunk of result) { if (chunk.choices?.[0]?.delta?.content) { process.stdout.write(chunk.choices[0].delta.content); } } } } main(); ``` ## How the user experience differs * **Progressive rendering**: you can display text as it arrives (“typing”), instead of waiting for a complete response. * **Visible work**: in tool/MCP workflows, you can show status updates (e.g., “Searching Exa…”) while the agent is calling tools. * **Interruptibility**: you can stop early (client-side) if the user already has what they need, instead of paying for a full completion. ## When to Stream Stream when: * Building chat interfaces where perceived latency matters * Generating long-form content (articles, code, analysis) * Running in terminals or logs where progress feedback helps Don’t stream when: * You need to parse the complete response before displaying * Using structured outputs with `.parse()` * Response time is already fast enough ## Next steps * **Route across models**: [Handoffs](/sdk/agents/handoffs) — Use fast/strong models by phase * **Add images last**: [Images & Vision](/sdk/agents/images) — Add multimodality when your text workflow is solid * **See patterns**: [Use Cases](/sdk/agents/use-cases/web-search-agent) — More streaming agent examples [Connect these docs programmatically](/ai-optimizations/using-mintlify-mcp) to Claude, VSCode, and more via MCP for real-time answers. # Structured Outputs Source: https://docs.dedaluslabs.ai/sdk/agents/structured-outputs Type-safe JSON responses with Pydantic, Zod, or Effect schemas LLMs generate text. Applications need data structures. Structured outputs bridge this gap—define a schema (Pydantic in Python, Zod or Effect Schema in TypeScript), and the Dedalus SDK ensures responses conform with full type safety. This is essential for building reliable applications. Instead of parsing free-form text and hoping for the best, you get validated objects that your code can trust. ## Extract typed data Define a schema. Call `.parse()`. Get validated objects. ```python Python (Pydantic) theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio from dedalus_labs import AsyncDedalus from dotenv import load_dotenv from pydantic import BaseModel load_dotenv() class Event(BaseModel): name: str city: str date: str class EventsResponse(BaseModel): query: str events: list[Event] async def main(): client = AsyncDedalus() completion = await client.chat.completions.parse( model="openai/gpt-5.2", messages=[{ "role": "user", "content": "Return 3 upcoming basketball events near San Francisco as JSON.", }], response_format=EventsResponse, ) parsed: EventsResponse = completion.choices[0].message.parsed print(parsed) if __name__ == "__main__": asyncio.run(main()) ``` ```typescript TypeScript (Zod) theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from "dedalus-labs"; import { zodResponseFormat } from "dedalus-labs/helpers/zod"; import { z } from "zod"; const client = new Dedalus(); const Event = z.object({ name: z.string(), city: z.string(), date: z.string(), }); const EventsResponse = z.object({ query: z.string(), events: z.array(Event), }); async function main() { const completion = await client.chat.completions.parse({ model: "openai/gpt-5.2", messages: [ { role: "user", content: "Return 3 upcoming basketball events near San Francisco as JSON.", }, ], response_format: zodResponseFormat(EventsResponse, "events_response"), }); console.log(completion.choices[0]?.message.parsed); } main(); ``` ## Advanced This section is a reference you can skim and come back to. It’s organized as a progression: 1. **Client `.parse()`** (non-streaming, typed output) 2. **Client `.stream()`** (streaming, typed output) 3. **Runner `response_format`** (typed output inside an agent/tool loop) 4. **Schemas & patterns** (optional fields, nested models, enums/unions) 5. **Structured tool calls** (when you need deterministic tool calling) ## Client API (reference) The client provides three methods for structured outputs: * **`.parse()`** - Non-streaming with type-safe schemas * **`.stream()`** - Streaming with type-safe schemas (context manager) * **`.create()`** - Dict-based schemas only ### TypeScript setup TypeScript schema helpers are optional peer dependencies. Install the validator you want to use: ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} bun install zod # or bun install effect ``` ### `.parse()` (non-streaming) This is the same pattern as the progressive example above, shown again in a more “API-reference” style. ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio from dedalus_labs import AsyncDedalus from dotenv import load_dotenv from pydantic import BaseModel load_dotenv() class Event(BaseModel): name: str city: str date: str class EventsResponse(BaseModel): query: str events: list[Event] async def main(): client = AsyncDedalus() completion = await client.chat.completions.parse( model="openai/gpt-5.2", messages=[ { "role": "user", "content": ( "Return 3 upcoming basketball events near San Francisco as JSON. " "Use ISO dates (YYYY-MM-DD)." ), } ], response_format=EventsResponse, mcp_servers=["tsion/exa"], # Web search via Exa ) parsed = completion.choices[0].message.parsed print(parsed) if __name__ == "__main__": asyncio.run(main()) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from 'dedalus-labs'; import { zodResponseFormat } from 'dedalus-labs/helpers/zod'; import { z } from 'zod'; const client = new Dedalus(); const Event = z.object({ name: z.string(), city: z.string(), date: z.string(), }); const EventsResponse = z.object({ query: z.string(), events: z.array(Event), }); async function main() { const completion = await client.chat.completions.parse({ model: 'openai/gpt-5.2', messages: [ { role: 'user', content: 'Return 3 upcoming basketball events near San Francisco as JSON. Use ISO dates (YYYY-MM-DD).', }, ], response_format: zodResponseFormat(EventsResponse, 'events_response'), mcpServers: ['tsion/exa'], // Web search via Exa }); console.log(completion.choices[0]?.message.parsed); } main(); ``` ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from 'dedalus-labs'; import { effectResponseFormat } from 'dedalus-labs/helpers/effect'; import * as Schema from 'effect/Schema'; const client = new Dedalus(); const Event = Schema.Struct({ name: Schema.String, city: Schema.String, date: Schema.String, }); async function main() { const completion = await client.chat.completions.parse({ model: 'openai/gpt-5.2', messages: [ { role: 'user', content: 'Return 3 upcoming basketball events near San Francisco as JSON. Use ISO dates (YYYY-MM-DD).', }, ], response_format: effectResponseFormat( Schema.Struct({ query: Schema.String, events: Schema.Array(Event) }), 'events_response', ), mcpServers: ['tsion/exa'], }); console.log(completion.choices[0]?.message.parsed); } main(); ``` ### `.stream()` (streaming) Use this when you want **streaming UX** and a **typed final result**. Streaming helpers differ by language: * **Python**: use `.stream(...)` as a context manager and read typed stream events. * **TypeScript**: stream tokens with `create({ stream: true, ... })`, then validate the final JSON with Zod/Effect. ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio from dedalus_labs import AsyncDedalus from dotenv import load_dotenv from pydantic import BaseModel load_dotenv() class Event(BaseModel): name: str city: str date: str class EventsResponse(BaseModel): query: str events: list[Event] async def main(): client = AsyncDedalus() # Use context manager for streaming async with client.chat.completions.stream( model="openai/gpt-5.2", messages=[{ "role": "user", "content": ( "Return 3 upcoming basketball events near San Francisco as JSON. " "Use ISO dates (YYYY-MM-DD)." ), }], response_format=EventsResponse, mcp_servers=["tsion/exa"], ) as stream: # Process events as they arrive async for event in stream: if event.type == "content.delta": print(event.delta, end="", flush=True) elif event.type == "content.done": # Snapshot available at content.done (typed) print(f"\nParsed events: {len(event.parsed.events)}") # Get final parsed result final = await stream.get_final_completion() parsed = final.choices[0].message.parsed print(f"\nFinal events: {len(parsed.events)}") if __name__ == "__main__": asyncio.run(main()) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from "dedalus-labs"; import { zodResponseFormat } from "dedalus-labs/helpers/zod"; import { z } from "zod"; const client = new Dedalus(); const Event = z.object({ name: z.string(), city: z.string(), date: z.string(), }); const EventsResponse = z.object({ query: z.string(), events: z.array(Event), }); async function main() { const stream = await client.chat.completions.create({ model: "openai/gpt-5.2", messages: [ { role: "user", content: "Return 3 upcoming basketball events near San Francisco as JSON. Use ISO dates (YYYY-MM-DD).", }, ], response_format: zodResponseFormat(EventsResponse, "events_response"), mcpServers: ["tsion/exa"], // Web search via Exa stream: true, }); // Stream output to the user while collecting it for parsing. let text = ""; for await (const chunk of stream) { const delta = chunk.choices?.[0]?.delta?.content; if (delta) { process.stdout.write(delta); text += delta; } } // If you need a typed object, parse the final JSON text. // (In production, use robust JSON extraction if your model outputs any extra text.) const parsed = EventsResponse.parse(JSON.parse(text)); console.log(`\nParsed events: ${parsed.events.length}`); } main(); ``` ### Optional Fields Use `Optional[T]` in Python, `.nullable()` in Zod, or `Schema.NullOr(...)` in Effect for nullable fields: With OpenAI strict mode, every field must be required. Model “optional” values as nullable. ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio from dedalus_labs import AsyncDedalus from dotenv import load_dotenv from pydantic import BaseModel load_dotenv() class Event(BaseModel): name: str city: str date: str price_usd: int | None = None # model unknown as null class EventsResponse(BaseModel): query: str events: list[Event] async def main(): client = AsyncDedalus() completion = await client.chat.completions.parse( model="openai/gpt-5.2", messages=[{ "role": "user", "content": ( "Return 3 upcoming basketball events near San Francisco as JSON. " "Include price_usd if known; otherwise null. Use ISO dates (YYYY-MM-DD)." ), }], response_format=EventsResponse, mcp_servers=["tsion/exa"], ) parsed = completion.choices[0].message.parsed for e in parsed.events: print(e.name, e.price_usd) if __name__ == "__main__": asyncio.run(main()) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from 'dedalus-labs'; import { zodResponseFormat } from 'dedalus-labs/helpers/zod'; import { z } from 'zod'; const client = new Dedalus(); const Event = z.object({ name: z.string(), city: z.string(), date: z.string(), price_usd: z.number().nullable(), }); const EventsResponse = z.object({ query: z.string(), events: z.array(Event), }); async function main() { const completion = await client.chat.completions.parse({ model: 'openai/gpt-5.2', messages: [ { role: 'user', content: 'Return 3 upcoming basketball events near San Francisco as JSON. Include price_usd if known; otherwise null. Use ISO dates (YYYY-MM-DD).', }, ], response_format: zodResponseFormat(EventsResponse, 'events_response'), mcpServers: ['tsion/exa'], }); const parsed = completion.choices[0]?.message.parsed; console.log(parsed?.events.map((e) => [e.name, e.price_usd])); } main(); ``` ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import * as Schema from 'effect/Schema'; const Event = Schema.Struct({ name: Schema.String, city: Schema.String, date: Schema.String, price_usd: Schema.NullOr(Schema.Number), }); const EventsResponse = Schema.Struct({ query: Schema.String, events: Schema.Array(Event), }); ``` Avoid `Schema.optional(...)` for structured outputs—use `Schema.NullOr(...)` instead. ## Schemas & patterns ## Nested Models ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio from dedalus_labs import AsyncDedalus from dotenv import load_dotenv from pydantic import BaseModel load_dotenv() class Venue(BaseModel): name: str address: str | None = None city: str class Event(BaseModel): name: str date: str venue: Venue class EventsResponse(BaseModel): query: str events: list[Event] async def main(): client = AsyncDedalus() completion = await client.chat.completions.parse( model="openai/gpt-5.2", messages=[{ "role": "user", "content": ( "Return 3 upcoming basketball events near San Francisco as JSON. " "Each event must include a nested venue object with name, city, and address (null if unknown). " "Use ISO dates (YYYY-MM-DD)." ) }], response_format=EventsResponse, mcp_servers=["tsion/exa"], ) parsed = completion.choices[0].message.parsed for e in parsed.events: print(e.name, "->", e.venue.name) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from "dedalus-labs"; import { zodResponseFormat } from "dedalus-labs/helpers/zod"; import { z } from "zod"; const client = new Dedalus(); const Venue = z.object({ name: z.string(), city: z.string(), address: z.string().nullable(), }); const Event = z.object({ name: z.string(), date: z.string(), venue: Venue, }); const EventsResponse = z.object({ query: z.string(), events: z.array(Event), }); async function main() { const completion = await client.chat.completions.parse({ model: "openai/gpt-5.2", messages: [ { role: "user", content: "Return 3 upcoming basketball events near San Francisco as JSON. Each event must include a nested venue object with name, city, and address (null if unknown). Use ISO dates (YYYY-MM-DD).", }, ], response_format: zodResponseFormat(EventsResponse, "events_response"), mcpServers: ["tsion/exa"], }); const parsed = completion.choices[0]?.message.parsed; console.log(parsed?.events.map((e) => [e.name, e.venue.name])); } main(); ``` ## Structured Tool Calls (advanced) Define type-safe tools with automatic argument parsing: ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio from dedalus_labs import AsyncDedalus from dotenv import load_dotenv from pydantic import BaseModel load_dotenv() class SearchEventsArgs(BaseModel): city: str month: str max_results: int = 5 async def main(): client = AsyncDedalus() tools = [ { "type": "function", "function": { "name": "search_events", "description": "Search for events in a city during a month.", "parameters": { "type": "object", "properties": { "city": {"type": "string"}, "month": {"type": "string", "description": "YYYY-MM"}, "max_results": {"type": "integer", "default": 5}, }, "required": ["city", "month"], "additionalProperties": False, }, "strict": True, } } ] completion = await client.chat.completions.parse( model="openai/gpt-5.2", messages=[{ "role": "user", "content": "Call search_events for San Francisco in 2026-01.", }], tools=tools, tool_choice={"type": "tool", "name": "search_events"}, ) message = completion.choices[0].message if message.tool_calls: tool_call = message.tool_calls[0] print(f"Tool called: {tool_call.function.name}") print(f"Parsed args: {tool_call.function.parsed_arguments}") if __name__ == "__main__": asyncio.run(main()) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from 'dedalus-labs'; import { zodFunction } from 'dedalus-labs/helpers/zod'; import { z } from 'zod'; const client = new Dedalus(); const SearchEventsTool = zodFunction({ name: 'search_events', parameters: z.object({ city: z.string(), month: z.string(), // YYYY-MM max_results: z.number().optional(), }), description: 'Search for events in a city during a month.', function: (args) => { // Your tool implementation would go here. // For docs, we return a placeholder JSON string. return JSON.stringify({ events: [], query: `${args.city} ${args.month}`, }); }, }); async function main() { const completion = await client.chat.completions.parse({ model: 'openai/gpt-5.2', messages: [{ role: 'user', content: 'Call search_events for San Francisco in 2026-01.' }], tools: [SearchEventsTool], // Force a deterministic tool call (useful for examples/tests). tool_choice: { type: 'tool', name: 'search_events' }, }); const toolCall = completion.choices[0]?.message.tool_calls?.[0]; if (toolCall) { console.log(`Tool called: ${toolCall.function.name}`); console.log(`Arguments: ${JSON.stringify(toolCall.function.parsed_arguments)}`); } } main(); ``` If you need deterministic tool calling, set `tool_choice` to one of the object variants: `{ type: 'auto' }` (model decides), `{ type: 'any' }` (require a tool call), `{ type: 'tool', name: 'search_events' }` (require a specific tool), `{ type: 'none' }` (disable tools). Passing the OpenAI string form (e.g. `tool_choice: 'required'`) will fail schema validation with a 422. ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import { effectFunction } from 'dedalus-labs/helpers/effect'; import * as Schema from 'effect/Schema'; const SearchEventsTool = effectFunction({ name: 'search_events', parameters: Schema.Struct({ city: Schema.String, month: Schema.String, // YYYY-MM max_results: Schema.NullOr(Schema.Number), }), description: 'Search for events in a city during a month.', }); ``` Tool parameters must be an object schema (use `Schema.Struct({ ... })`). ## Enums and Unions ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio from typing import Literal from dedalus_labs import AsyncDedalus from dotenv import load_dotenv from pydantic import BaseModel load_dotenv() class Event(BaseModel): name: str city: str date: str category: Literal["sports", "music", "theater", "other"] ticket_status: Literal["available", "sold_out", "unknown"] class EventsResponse(BaseModel): query: str events: list[Event] async def main(): client = AsyncDedalus() completion = await client.chat.completions.parse( model="openai/gpt-5.2", messages=[{ "role": "user", "content": ( "Return 3 upcoming events near San Francisco as JSON. " "Each event must include category (sports/music/theater/other) and ticket_status (available/sold_out/unknown). " "Use ISO dates (YYYY-MM-DD)." ) }], response_format=EventsResponse, mcp_servers=["tsion/exa"], ) parsed = completion.choices[0].message.parsed for e in parsed.events: print(e.name, e.category, e.ticket_status) if __name__ == "__main__": asyncio.run(main()) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from "dedalus-labs"; import { zodResponseFormat } from "dedalus-labs/helpers/zod"; import { z } from "zod"; const client = new Dedalus(); const Event = z.object({ name: z.string(), city: z.string(), date: z.string(), category: z.enum(["sports", "music", "theater", "other"]), ticket_status: z.union([z.literal("available"), z.literal("sold_out"), z.literal("unknown")]), }); const EventsResponse = z.object({ query: z.string(), events: z.array(Event), }); async function main() { const completion = await client.chat.completions.parse({ model: "openai/gpt-5.2", messages: [ { role: "user", content: "Return 3 upcoming events near San Francisco as JSON. Each event must include category (sports/music/theater/other) and ticket_status (available/sold_out/unknown). Use ISO dates (YYYY-MM-DD).", }, ], response_format: zodResponseFormat(EventsResponse, "events_response"), mcpServers: ["tsion/exa"], }); const parsed = completion.choices[0]?.message.parsed; console.log(parsed?.events.map((e) => [e.name, e.category, e.ticket_status])); } main(); ``` ## DedalusRunner API The Runner supports `response_format` with automatic schema conversion: ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio from dedalus_labs import AsyncDedalus, DedalusRunner from dotenv import load_dotenv from pydantic import BaseModel load_dotenv() class Event(BaseModel): name: str city: str date: str class EventsResponse(BaseModel): query: str events: list[Event] def as_bullets(items: list[str]) -> str: """Format items as a bulleted list.""" return "\n".join(f"• {item}" for item in items) async def main(): client = AsyncDedalus() runner = DedalusRunner(client) result = await runner.run( input=( "Find me the nearest basketball games in January in San Francisco. " "Then call as_bullets with a list of items (one per event: name, city, date)." ), model="anthropic/claude-opus-4-5", mcp_servers=["tsion/exa"], # Web search via Exa tools=[as_bullets], response_format=EventsResponse, max_steps=5, ) print(result.final_output) if __name__ == "__main__": asyncio.run(main()) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from 'dedalus-labs'; import { DedalusRunner } from 'dedalus-labs'; const client = new Dedalus(); function asBullets(items: string[]): string { return items.map((item) => `• ${item}`).join('\n'); } async function main() { const runner = new DedalusRunner(client, true); const result = await runner.run({ model: 'anthropic/claude-opus-4-5', input: 'Find me the nearest basketball games in January in San Francisco using Ticketmaster. Then call asBullets with a list of items (one per event: name, city, date).', mcpServers: ['tsion/exa'], // Web search via Exa tools: [asBullets], maxSteps: 5, }); console.log(result.finalOutput); } main(); ``` ## .create() vs .parse() vs .stream() | Method | Schema Support | Streaming | Use Case | | ----------- | ------------------- | --------- | ----------------------- | | `.create()` | Dict only | ✓ | Manual JSON schemas | | `.parse()` | Pydantic/Zod/Effect | ❌ | Type-safe non-streaming | | `.stream()` | Pydantic/Zod/Effect | ✓ | Type-safe streaming | `.create()` expects a plain JSON Schema object. Don’t pass a Pydantic model, Zod schema, or Effect schema directly. “Streaming + typed output” is language-dependent: - **Python**: `.stream(...)` yields typed events and a typed final snapshot. - **TypeScript**: stream tokens and validate the final JSON with Zod/Effect. ## Error Handling ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio from dedalus_labs import AsyncDedalus from dotenv import load_dotenv from pydantic import BaseModel load_dotenv() class Event(BaseModel): name: str city: str date: str class EventsResponse(BaseModel): query: str events: list[Event] async def main(): client = AsyncDedalus() try: completion = await client.chat.completions.parse( model="openai/gpt-5.2", messages=[{ "role": "user", "content": ( "Return 3 upcoming basketball events near San Francisco as JSON. " "Use ISO dates (YYYY-MM-DD)." ), }], response_format=EventsResponse, ) parsed = completion.choices[0].message.parsed print(f"Parsed events: {len(parsed.events)}") except Exception as e: print("Parse failed:", e) if __name__ == "__main__": asyncio.run(main()) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from 'dedalus-labs'; import { zodResponseFormat } from 'dedalus-labs/helpers/zod'; import { z } from 'zod'; const client = new Dedalus(); const Event = z.object({ name: z.string(), city: z.string(), date: z.string(), }); const EventsResponse = z.object({ query: z.string(), events: z.array(Event), }); async function main() { try { const completion = await client.chat.completions.parse({ model: 'openai/gpt-5.2', messages: [ { role: 'user', content: 'Return 3 upcoming basketball events near San Francisco as JSON. Use ISO dates (YYYY-MM-DD).', }, ], response_format: zodResponseFormat(EventsResponse, 'events_response'), }); const parsed = completion.choices[0]?.message.parsed; console.log(`Parsed events: ${parsed?.events.length ?? 0}`); } catch (error) { console.error('Request failed:', error); } } main(); ``` ## Supported Models The Dedalus SDK's `.parse()` and `.stream()` methods work across all providers. Schema enforcement varies: **Strict Enforcement** (CFG-based, schema guarantees): * ✓ `openai/*` - Context-free grammar compilation * ✓ `xai/*` - Native schema validation * ✓ `fireworks_ai/*` - Native schema validation (select models) * ✓ `deepseek/*` - Native schema validation (select models) **Best-Effort** (schema sent for guidance, no guarantees): * 🟡 `google/*` - Schema forwarded to `generationConfig.responseSchema` * 🟡 `anthropic/*` - Prompt-based JSON generation (\~85-90% success rate) For `google/*` and `anthropic/*` models, always validate parsed output and implement retry logic. ## Provider Examples You can use `.parse()` and `.stream()` with models from any provider. In practice, you only change `model`—everything else stays the same. For a full list of model IDs, see the [providers guide](/sdk/agents/guides/providers). ## Quick Reference ### Python (Pydantic) ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_labs import AsyncDedalus from pydantic import BaseModel class MyModel(BaseModel): field: str client = AsyncDedalus() result = await client.chat.completions.parse( model="openai/gpt-5.2", messages=[...], response_format=MyModel, ) parsed = result.choices[0].message.parsed ``` ### TypeScript (Zod) ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from 'dedalus-labs'; import { zodResponseFormat } from 'dedalus-labs/helpers/zod'; import { z } from 'zod'; const MySchema = z.object({ field: z.string() }); const client = new Dedalus(); const result = await client.chat.completions.parse({ model: 'openai/gpt-5.2', messages: [...], response_format: zodResponseFormat(MySchema, 'my_schema'), }); const parsed = result.choices[0]?.message.parsed; ``` ### TypeScript (Effect Schema) ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from 'dedalus-labs'; import { effectResponseFormat } from 'dedalus-labs/helpers/effect'; import * as Schema from 'effect/Schema'; const MySchema = Schema.Struct({ field: Schema.String }); const client = new Dedalus(); const result = await client.chat.completions.parse({ model: 'openai/gpt-5.2', messages: [...], response_format: effectResponseFormat(MySchema, 'my_schema'), }); const parsed = result.choices[0]?.message.parsed; ``` ### Zod Helpers ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import { zodResponseFormat, zodFunction } from 'dedalus-labs/helpers/zod'; // For response schemas zodResponseFormat(MyZodSchema, 'schema_name') // For tool definitions zodFunction({ name: 'tool_name', description: 'What the tool does', parameters: z.object({ ... }), function: (args) => { ... }, }) ``` ### Effect Helpers ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import { effectResponseFormat, effectFunction } from 'dedalus-labs/helpers/effect'; // For response schemas effectResponseFormat(MyEffectSchema, 'schema_name') // For tool definitions effectFunction({ name: 'tool_name', description: 'What the tool does', parameters: MyEffectParametersSchema, function: (args) => { ... }, }) ``` If you still use `@effect/schema`, schemas from `@effect/schema/Schema` also work with `helpers/effect`. You still need to install `effect` (the Dedalus SDK uses `effect/JSONSchema` and `effect/Schema` for conversion + validation). Prefer `effect/Schema` for new code. ## Next steps * **Stream output**: [Streaming](/sdk/agents/streaming) — Improve UX for long tool/MCP runs * **Route across models**: [Handoffs](/sdk/agents/handoffs) — Use fast/strong models by phase * **See patterns**: [Use Cases](/sdk/agents/use-cases/data-analyst) — Structured extraction workflows [Connect these docs programmatically](/ai-optimizations/using-mintlify-mcp) to Claude, VSCode, and more via MCP for real-time answers. # Tools Source: https://docs.dedaluslabs.ai/sdk/agents/tools Give agents the ability to take actions Agents become useful when they can do things beyond generating text. Tools let them call functions, query databases, make API requests—anything you can express in code. ## How It Works Define a function with type hints and a docstring. Pass it to `runner.run()`. The Dedalus SDK extracts the schema automatically and handles execution when the model decides to use it. ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio from dedalus_labs import AsyncDedalus, DedalusRunner from dotenv import load_dotenv load_dotenv() def as_bullets(items: list[str]) -> str: """Format items as a bulleted list.""" return "\n".join(f"• {item}" for item in items) async def main(): client = AsyncDedalus() runner = DedalusRunner(client) result = await runner.run( input=( "Take the following events and call as_bullets with a list of items (one per event).\n\n" "Events:\n" "- Warriors vs Lakers — San Francisco — 2026-01-18\n" "- Warriors vs Suns — San Francisco — 2026-01-22\n" "- Warriors vs Celtics — San Francisco — 2026-01-29\n\n" "Return only the list." ), model="openai/gpt-5.2", tools=[as_bullets], ) print(result.final_output) if __name__ == "__main__": asyncio.run(main()) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from 'dedalus-labs'; import { DedalusRunner } from 'dedalus-labs'; const client = new Dedalus(); const runner = new DedalusRunner(client, true); function formatTable(rows: Record[]): string { if (!rows.length) return 'No results.'; const cols = Object.keys(rows[0]); const header = `| ${cols.join(' | ')} |`; const sep = `| ${cols.map(() => '---').join(' | ')} |`; const body = rows.map((r) => `| ${cols.map((c) => String(r?.[c] ?? '')).join(' | ')} |`); return [header, sep, ...body].join('\n'); } async function main() { const result = await runner.run({ input: 'Take the following events and call formatTable with a list of rows (one row per event).\n\n' + 'Events:\n' + '- {"name":"Warriors vs Lakers","city":"San Francisco","date":"2026-01-18"}\n' + '- {"name":"Warriors vs Suns","city":"San Francisco","date":"2026-01-22"}\n' + '- {"name":"Warriors vs Celtics","city":"San Francisco","date":"2026-01-29"}\n\n' + 'Return only the table.', model: 'openai/gpt-5.2', tools: [formatTable], }); console.log(result.finalOutput); } main(); ``` The model sees the tool schemas, decides which to call, and the Runner executes them. Multi-step reasoning happens automatically—the Runner keeps calling the model until it can complete the task. ## Tool best practices Good tools typically have: * **Type hints** on all parameters and return values * **Docstrings** that explain what the tool does (the model reads these) * **Clear names** that indicate purpose ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} # Good: typed, documented, clear name def get_weather(city: str, units: str = "celsius") -> dict: """Get current weather for a city. Returns temperature and conditions.""" return {"temp": 22, "conditions": "sunny"} # Bad: no types, no docs, unclear name def do_thing(x): return some_api_call(x) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} // Good: typed, documented, clear name function getWeather(city: string, units: string = 'celsius'): object { // Get current weather for a city return { temp: 22, conditions: 'sunny' }; } // Bad: no types, unclear name function doThing(x: any) { return someApiCall(x); } ``` ## Async Tools Tools can be async. The Runner awaits them automatically: ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} async def fetch_user(user_id: int) -> dict: """Fetch user profile from database.""" async with db.connection() as conn: return await conn.fetchone("SELECT * FROM users WHERE id = $1", user_id) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} async function fetchUser(userId: number): Promise { // Fetch user profile from database const result = await db.query("SELECT * FROM users WHERE id = $1", [userId]); return result.rows[0]; } ``` ## Agent as Tool Wrap a specialized agent as a tool. The coordinator delegates specific tasks to specialists without giving up conversation control. This differs from [handoffs](/sdk/agents/handoffs): * **Handoffs**: New agent takes over the conversation with full history * **Agent as tool**: Specialist receives specific input, returns output, coordinator continues ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio from dedalus_labs import AsyncDedalus, DedalusRunner async def main(): client = AsyncDedalus() runner = DedalusRunner(client) # Specialist: wrap another runner call as a tool async def research_specialist(query: str) -> str: """Deep research on a topic. Use for questions requiring thorough analysis.""" result = await runner.run( input=query, model="openai/gpt-5.2", # Stronger model for research instructions="You are a research analyst. Be thorough and cite sources.", mcp_servers=["tsion/exa"] # Web search via Exa ) return result.final_output async def code_specialist(spec: str) -> str: """Generate production code from specifications.""" result = await runner.run( input=spec, model="anthropic/claude-opus-4-5", # Strong at code instructions="Write clean, tested, production-ready code." ) return result.final_output # Coordinator: cheap model that delegates to specialists result = await runner.run( input="Research quantum computing breakthroughs in 2025, then write a Python simulator for a basic quantum gate", model="openai/gpt-4o-mini", tools=[research_specialist, code_specialist] ) print(result.final_output) if __name__ == "__main__": asyncio.run(main()) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from 'dedalus-labs'; import { DedalusRunner } from 'dedalus-labs'; const client = new Dedalus(); const runner = new DedalusRunner(client); // Specialist functions async function researchSpecialist(query: string): Promise { const result = await runner.run({ input: query, model: 'openai/gpt-4o', instructions: 'You are a research analyst. Be thorough.', mcpServers: ['tsion/exa'], // Web search via Exa }); return result.finalOutput; } async function codeSpecialist(spec: string): Promise { const result = await runner.run({ input: spec, model: 'anthropic/claude-opus-4-5', instructions: 'Write clean, production-ready code.', }); return result.finalOutput; } // Coordinator delegates to specialists const result = await runner.run({ input: 'Research AI trends, then write a TypeScript example', model: 'openai/gpt-5.2', tools: [researchSpecialist, codeSpecialist], }); ``` **When to use this pattern:** | Scenario | Why Agent-as-Tool | | ------------------ | --------------------------------------------------------- | | Vision/OCR tasks | Text-only coordinator delegates images to vision model | | Code generation | Fast model triages, strong model writes code | | Domain specialists | Generic router -> specialized instructions/model | | Cost optimization | Cheap coordinator, expensive specialists only when needed | ## Model Selection Tool calling quality varies by model. For reliable multi-step tool use: `openai/gpt-5.2` and `openai/gpt-4.1` handle complex tool chains well. Older or smaller models may struggle with multi-step reasoning. ## Next steps * **Route across models**: [Handoffs](/sdk/agents/handoffs) -- Route to different models mid-conversation * **Return typed data**: [Structured Outputs](/sdk/agents/structured-outputs) -- Validate and parse JSON into schemas * **Stream progress**: [Streaming](/sdk/agents/streaming) -- Show tool-call progress in real time * **See full examples**: [Use Cases](/sdk/agents/use-cases/web-search-agent) -- End-to-end agent patterns [Connect these docs programmatically](/ai-optimizations/using-mintlify-mcp) to Claude, VSCode, and more via MCP for real-time answers. # TypeScript SDK Source: https://docs.dedaluslabs.ai/sdk/agents/typescript Agents API TypeScript SDK v0.1.0-alpha.8 **v0.1.0-alpha.8** | [GitHub](https://github.com/dedalus-labs/dedalus-sdk-typescript) | [Changelog](https://github.com/dedalus-labs/dedalus-sdk-typescript/blob/main/CHANGELOG.md) ## Installation ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} npm install dedalus-labs ``` ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} pnpm add dedalus-labs ``` ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} yarn add dedalus-labs ``` ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} bun add dedalus-labs ``` ## Usage See the full method reference in the [API Reference](/api-reference/dcs) tab. ```js theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from 'dedalus-labs'; const client = new Dedalus({ apiKey: process.env['DEDALUS_API_KEY'], // This is the default and can be omitted environment: 'development', // defaults to 'production' }); const completion = await client.chat.completions.create({ model: 'openai/gpt-5-nano', messages: [ { role: 'system', content: 'You are Stephen Dedalus. Respond in morose Joycean malaise.' }, { role: 'user', content: 'Hello, how are you today?' }, ], }); console.log(completion.id); ``` ## Streaming We provide support for streaming responses using Server Sent Events (SSE). ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from 'dedalus-labs'; const client = new Dedalus(); const stream = await client.chat.completions.create({ model: 'openai/gpt-5-nano', stream: true, messages: [ { role: 'system', content: 'You are Stephen Dedalus. Respond in morose Joycean malaise.' }, { role: 'user', content: 'What do you think of artificial intelligence?' }, ], }); for await (const streamChunk of stream) { console.log(streamChunk.id); } ``` If you need to cancel a stream, you can `break` from the loop or call `stream.controller.abort()`. ### Request & Response types This library includes TypeScript definitions for all request params and response fields. You may import and use them like so: ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from 'dedalus-labs'; const client = new Dedalus({ apiKey: process.env['DEDALUS_API_KEY'], // This is the default and can be omitted environment: 'development', // defaults to 'production' }); const params: Dedalus.Chat.CompletionCreateParams = { model: 'openai/gpt-5-nano', messages: [ { role: 'system', content: 'You are Stephen Dedalus. Respond in morose Joycean malaise.' }, { role: 'user', content: 'Hello, how are you today?' }, ], }; const completion: Dedalus.Chat.Completion = await client.chat.completions.create(params); ``` Documentation for each method, request param, and response field are available in docstrings and will appear on hover in most modern editors. ## File uploads Request parameters that correspond to file uploads can be passed in many different forms: * `File` (or an object with the same structure) * a `fetch` `Response` (or an object with the same structure) * an `fs.ReadStream` * the return value of our `toFile` helper ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}} import fs from 'fs'; import Dedalus, { toFile } from 'dedalus-labs'; const client = new Dedalus(); // If you have access to Node `fs` we recommend using `fs.createReadStream()`: await client.audio.transcriptions.create({ file: fs.createReadStream('/path/to/file'), model: 'model' }); // Or if you have the web `File` API you can pass a `File` instance: await client.audio.transcriptions.create({ file: new File(['my bytes'], 'file'), model: 'model' }); // You can also pass a `fetch` `Response`: await client.audio.transcriptions.create({ file: await fetch('https://somesite/file'), model: 'model' }); // Finally, if none of the above are convenient, you can use our `toFile` helper: await client.audio.transcriptions.create({ file: await toFile(Buffer.from('my bytes'), 'file'), model: 'model', }); await client.audio.transcriptions.create({ file: await toFile(new Uint8Array([0, 1, 2]), 'file'), model: 'model', }); ``` ## Error Handling Always wrap API calls in try/catch. The SDK throws typed errors for HTTP failures. When the library is unable to connect to the API, or if the API returns a non-success status code (i.e., 4xx or 5xx response), a subclass of `APIError` will be thrown: ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}} const completion = await client.chat.completions .create({ model: 'openai/gpt-5-nano', messages: [ { role: 'system', content: 'You are Stephen Dedalus. Respond in morose Joycean malaise.' }, { role: 'user', content: 'Hello, how are you today?' }, ], }) .catch(async (err) => { if (err instanceof Dedalus.APIError) { console.log(err.status); // 400 console.log(err.name); // BadRequestError console.log(err.headers); // {server: 'nginx', ...} } else { throw err; } }); ``` Error codes are as follows: | Status Code | Error Type | | ----------- | -------------------------- | | 400 | `BadRequestError` | | 401 | `AuthenticationError` | | 403 | `PermissionDeniedError` | | 404 | `NotFoundError` | | 422 | `UnprocessableEntityError` | | 429 | `RateLimitError` | | >=500 | `InternalServerError` | | N/A | `APIConnectionError` | ### Retries Certain errors will be automatically retried 2 times by default, with a short exponential backoff. Connection errors (for example, due to a network connectivity problem), 408 Request Timeout, 409 Conflict, 429 Rate Limit, and >=500 Internal errors will all be retried by default. You can use the `maxRetries` option to configure or disable this: ```js theme={"theme":{"light":"github-light","dark":"github-dark"}} // Configure the default for all requests: const client = new Dedalus({ maxRetries: 0, // default is 2 }); // Or, configure per-request: await client.chat.completions.create({ model: 'openai/gpt-5-nano', messages: [{ role: 'system', content: 'You are Stephen Dedalus. Respond in morose Joycean malaise.' }, { role: 'user', content: 'Hello, how are you today?' }] }, { maxRetries: 5, }); ``` ### Timeouts Requests time out after 1 minute by default. You can configure this with a `timeout` option: ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}} // Configure the default for all requests: const client = new Dedalus({ timeout: 20 * 1000, // 20 seconds (default is 1 minute) }); // Override per-request: await client.chat.completions.create({ model: 'openai/gpt-5-nano', messages: [{ role: 'system', content: 'You are Stephen Dedalus. Respond in morose Joycean malaise.' }, { role: 'user', content: 'Hello, how are you today?' }] }, { timeout: 5 * 1000, }); ``` On timeout, an `APIConnectionTimeoutError` is thrown. Note that requests which time out will be [retried twice by default](#retries). ## Default Headers We automatically send the following headers with all requests. | Header | Value | | --------------- | ------------- | | `User-Agent` | `Dedalus-SDK` | | `X-SDK-Version` | `1.0.0` | If you need to, you can override these headers by setting default headers on a per-request basis. ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from 'dedalus-labs'; const client = new Dedalus(); const completion = await client.chat.completions.create( { model: 'openai/gpt-5-nano', messages: [ { role: 'system', content: 'You are Stephen Dedalus. Respond in morose Joycean malaise.' }, { role: 'user', content: 'Hello, how are you today?' }, ], }, { headers: { 'User-Agent': 'My-Custom-Value' } }, ); ``` ### Accessing raw Response data (e.g., headers) The "raw" `Response` returned by `fetch()` can be accessed through the `.asResponse()` method on the `APIPromise` type that all methods return. This method returns as soon as the headers for a successful response are received and does not consume the response body, so you are free to write custom parsing or streaming logic. You can also use the `.withResponse()` method to get the raw `Response` along with the parsed data. Unlike `.asResponse()` this method consumes the body, returning once it is parsed. ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}} const client = new Dedalus(); const response = await client.chat.completions .create({ model: 'openai/gpt-5-nano', messages: [ { role: 'system', content: 'You are Stephen Dedalus. Respond in morose Joycean malaise.' }, { role: 'user', content: 'Hello, how are you today?' }, ], }) .asResponse(); console.log(response.headers.get('X-My-Header')); console.log(response.statusText); // access the underlying Response object const { data: completion, response: raw } = await client.chat.completions .create({ model: 'openai/gpt-5-nano', messages: [ { role: 'system', content: 'You are Stephen Dedalus. Respond in morose Joycean malaise.' }, { role: 'user', content: 'Hello, how are you today?' }, ], }) .withResponse(); console.log(raw.headers.get('X-My-Header')); console.log(completion.id); ``` ### Logging > \[!IMPORTANT] > All log messages are intended for debugging only. The format and content of log messages > may change between releases. #### Log levels The log level can be configured in two ways: 1. Via the `DEDALUS_LOG` environment variable 2. Using the `logLevel` client option (overrides the environment variable if set) ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from 'dedalus-labs'; const client = new Dedalus({ logLevel: 'debug', // Show all log messages }); ``` Available log levels, from most to least verbose: * `'debug'` - Show debug messages, info, warnings, and errors * `'info'` - Show info messages, warnings, and errors * `'warn'` - Show warnings and errors (default) * `'error'` - Show only errors * `'off'` - Disable all logging At the `'debug'` level, all HTTP requests and responses are logged, including headers and bodies. Some authentication-related headers are redacted, but sensitive data in request and response bodies may still be visible. #### Custom logger By default, this library logs to `globalThis.console`. You can also provide a custom logger. Most logging libraries are supported, including [pino](https://www.npmjs.com/package/pino), [winston](https://www.npmjs.com/package/winston), [bunyan](https://www.npmjs.com/package/bunyan), [consola](https://www.npmjs.com/package/consola), [signale](https://www.npmjs.com/package/signale), and [@std/log](https://jsr.io/@std/log). If your logger doesn't work, please open an issue. When providing a custom logger, the `logLevel` option still controls which messages are emitted, messages below the configured level will not be sent to your logger. ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from 'dedalus-labs'; import pino from 'pino'; const logger = pino(); const client = new Dedalus({ logger: logger.child({ name: 'Dedalus' }), logLevel: 'debug', // Send all messages to pino, allowing it to filter }); ``` ### Making custom/undocumented requests This library is typed for convenient access to the documented API. If you need to access undocumented endpoints, params, or response properties, the library can still be used. #### Undocumented endpoints To make requests to undocumented endpoints, you can use `client.get`, `client.post`, and other HTTP verbs. Options on the client, such as retries, will be respected when making these requests. ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}} await client.post('/some/path', { body: { some_prop: 'foo' }, query: { some_query_arg: 'bar' }, }); ``` #### Undocumented request params To make requests using undocumented parameters, you may use `// @ts-expect-error` on the undocumented parameter. This library doesn't validate at runtime that the request matches the type, so any extra values you send will be sent as-is. ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}} client.chat.completions.create({ // ... // @ts-expect-error baz is not yet public baz: 'undocumented option', }); ``` For requests with the `GET` verb, any extra params will be in the query, all other requests will send the extra param in the body. If you want to explicitly send an extra argument, you can do so with the `query`, `body`, and `headers` request options. #### Undocumented response properties To access undocumented response properties, you may access the response object with `// @ts-expect-error` on the response object, or cast the response object to the requisite type. Like the request params, we do not validate or strip extra properties from the response from the API. ### Customizing the fetch client By default, this library expects a global `fetch` function is defined. If you want to use a different `fetch` function, you can either polyfill the global: ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}} import fetch from 'my-fetch'; globalThis.fetch = fetch; ``` Or pass it to the client: ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from 'dedalus-labs'; import fetch from 'my-fetch'; const client = new Dedalus({ fetch }); ``` ### Fetch options If you want to set custom `fetch` options without overriding the `fetch` function, you can provide a `fetchOptions` object when instantiating the client or making a request. (Request-specific options override client options.) ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from 'dedalus-labs'; const client = new Dedalus({ fetchOptions: { // `RequestInit` options }, }); ``` #### Configuring proxies To modify proxy behavior, you can provide custom `fetchOptions` that add runtime-specific proxy options to requests: **Node** \[[docs](https://github.com/nodejs/undici/blob/main/docs/docs/api/ProxyAgent.md)] ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from 'dedalus-labs'; import * as undici from 'undici'; const proxyAgent = new undici.ProxyAgent('http://localhost:8888'); const client = new Dedalus({ fetchOptions: { dispatcher: proxyAgent, }, }); ``` **Bun** \[[docs](https://bun.sh/guides/http/proxy)] ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from 'dedalus-labs'; const client = new Dedalus({ fetchOptions: { proxy: 'http://localhost:8888', }, }); ``` **Deno** \[[docs](https://docs.deno.com/api/deno/~/Deno.createHttpClient)] ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from 'npm:dedalus-labs'; const httpClient = Deno.createHttpClient({ proxy: { url: 'http://localhost:8888' } }); const client = new Dedalus({ fetchOptions: { client: httpClient, }, }); ``` ## Frequently Asked Questions ## Semantic versioning This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions: 1. Changes that only affect static types, without breaking runtime behavior. 2. Changes to library internals which are technically public but not intended or documented for external use. *(Please open a GitHub issue to let us know if you are relying on such internals.)* 3. Changes that we do not expect to impact the vast majority of users in practice. We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience. We are keen for your feedback; please open an [issue](https://www.github.com/dedalus-labs/dedalus-sdk-typescript/issues) with questions, bugs, or suggestions. TypeScript >= 4.9 is supported. The following runtimes are supported: * Web browsers (Up-to-date Chrome, Firefox, Safari, Edge, and more) * Node.js 20 LTS or later ([non-EOL](https://endoflife.date/nodejs)) versions. * Deno v1.28.0 or higher. * Bun 1.0 or later. * Cloudflare Workers. * Vercel Edge Runtime. * Jest 28 or greater with the `"node"` environment (`"jsdom"` is not supported at this time). * Nitro v2.6 or greater. Note that React Native is not supported at this time. If you are interested in other runtime environments, please open or upvote an issue on GitHub. # Concert Planner Source: https://docs.dedaluslabs.ai/sdk/agents/use-cases/concert-planner Find concerts and venue information Finding concert tickets involves checking dates, venues, seating options, and accessibility. An agent with web search can consolidate this across multiple sources. ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio from dedalus_labs import AsyncDedalus, DedalusRunner from dotenv import load_dotenv load_dotenv() async def main(): client = AsyncDedalus() runner = DedalusRunner(client) result = await runner.run( input="""I want to see Taylor Swift in New York City. Help me find: 1. Upcoming concert dates 2. Venue details 3. Ticket price ranges 4. Accessibility information 5. Best seating options for the budget""", model="openai/gpt-4.1", mcp_servers=["tsion/exa"] # Web search via Exa ) print(result.final_output) if __name__ == "__main__": asyncio.run(main()) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus, { DedalusRunner } from 'dedalus-labs'; import * as dotenv from 'dotenv'; dotenv.config(); async function main() { const client = new Dedalus({ apiKey: process.env.DEDALUS_API_KEY }); const runner = new DedalusRunner(client); const result = await runner.run({ input: `I want to see Taylor Swift in New York City. Help me find: 1. Upcoming concert dates 2. Venue details 3. Ticket price ranges 4. Accessibility information 5. Best seating options for the budget`, model: 'openai/gpt-4.1', mcpServers: ['tsion/exa'] // Web search via Exa }); console.log(result.finalOutput); } main(); ``` ## Exa Search MCP The `tsion/exa` server provides semantic web search, useful for finding: * Event listings by artist, venue, or location * Venue information and reviews * Ticket availability across platforms * Event details and timing ## When to Use This pattern works for any ticketed event: sports games, theater, festivals. The agent handles the search and comparison work, presenting options that match your criteria instead of making you browse through pages of results. # Data Analyst Source: https://docs.dedaluslabs.ai/sdk/agents/use-cases/data-analyst Create a data analyst agent that can search for real-time data, write and execute Python code to analyze it, and generate insights. ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio from dedalus_labs import AsyncDedalus, DedalusRunner from dedalus_labs.utils.stream import stream_async from dotenv import load_dotenv load_dotenv() def execute_python_code(code: str) -> str: """ Execute Python code and return the result. Safely executes code in a controlled namespace. """ try: namespace = {} exec(code, {"__builtins__": __builtins__}, namespace) if 'result' in namespace: return str(namespace['result']) results = {k: v for k, v in namespace.items() if not k.startswith('_')} return str(results) if results else "Code executed successfully" except Exception as e: return f"Error executing code: {str(e)}" async def main(): client = AsyncDedalus() runner = DedalusRunner(client) result = runner.run( input="""Research the current stock price of Tesla (TSLA) and Apple (AAPL). Then write and execute Python code to: 1. Compare their current prices 2. Calculate the percentage difference 3. Determine which stock has grown more in the past year based on the data you find 4. Provide investment insights based on your analysis Use web search to get the latest stock information.""", model="openai/gpt-5", tools=[execute_python_code], mcp_servers=["windsor/brave-search-mcp"], stream=True ) await stream_async(result) if __name__ == "__main__": asyncio.run(main()) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus, { DedalusRunner } from "dedalus-labs"; import * as dotenv from "dotenv"; dotenv.config(); function executePythonCode(code: string): string { // Note: In TypeScript, you would typically use a sandboxed // execution environment or call out to a Python service return `Code execution requested: ${code.substring(0, 100)}...`; } async function main() { const client = new Dedalus({ apiKey: process.env.DEDALUS_API_KEY, }); const runner = new DedalusRunner(client); const result = await runner.run({ input: `Research the current stock price of Tesla (TSLA) and Apple (AAPL). Then write and execute Python code to: 1. Compare their current prices 2. Calculate the percentage difference 3. Determine which stock has grown more in the past year based on the data you find 4. Provide investment insights based on your analysis Use web search to get the latest stock information.`, model: "openai/gpt-5", tools: [executePythonCode], mcpServers: ["windsor/brave-search-mcp"], }); console.log(result.finalOutput); } main(); ``` This data analyst example combines real-time web search with code execution capabilities: * **Brave Search MCP** (`windsor/brave-search-mcp`): Fetches real-time data from the web * **execute\_python\_code** tool: Allows the agent to write and run Python code for analysis The agent can search for current information, extract relevant data, then dynamically write code to analyze it and generate insights. **Note**: In production environments, consider using sandboxed code execution for security. # Travel Agent Source: https://docs.dedaluslabs.ai/sdk/agents/use-cases/travel-agent Creating a travel planning agent that can search for flights, hotels, and provide travel recommendations. ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio from dedalus_labs import AsyncDedalus, DedalusRunner from dotenv import load_dotenv load_dotenv() async def main(): client = AsyncDedalus() runner = DedalusRunner(client) result = await runner.run( input="""I'm planning a trip to Paris, France from San Francisco, CA for 3 days for Christmas in 2025. Can you help me find: 1. Flight options and prices, give me the best option for the cheapest flight 2. Hotel recommendations in central Paris 3. Weather forecast for my travel dates 4. Popular events during the Christmas season in Paris 5. Give a quick summary of the trip and the results My budget is around $3000 total and I prefer mid-range accommodations. keep it succint in 300 words or less""", model="anthropic/claude-opus-4-5", mcp_servers=[ "windsor/brave-search-mcp", # For travel information search "windsor/open-meteo-mcp", # For weather at destination "tsion/exa" # For events lookup ] ) print(f"Travel Planning Results:\n{result.final_output}") if __name__ == "__main__": asyncio.run(main()) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus, { DedalusRunner } from "dedalus-labs"; import * as dotenv from "dotenv"; dotenv.config(); async function main() { const client = new Dedalus({ apiKey: process.env.DEDALUS_API_KEY, }); const runner = new DedalusRunner(client); const result = await runner.run({ input: `I'm planning a trip to Paris, France from San Francisco, CA for 3 days for Christmas in 2025. Can you help me find: 1. Flight options and prices, give me the best option for the cheapest flight 2. Hotel recommendations in central Paris 3. Weather forecast for my travel dates 4. Popular events during the Christmas season in Paris 5. Give a quick summary of the trip and the results My budget is around $3000 total and I prefer mid-range accommodations. keep it succint in 300 words or less`, model: "anthropic/claude-opus-4-5", mcpServers: [ "windsor/brave-search-mcp", // For travel information search "windsor/open-meteo-mcp", // For weather at destination "tsion/exa", // For events lookup ], }); console.log(`Travel Planning Results:\n${result.finalOutput}`); } main(); ``` This travel agent example uses multiple MCP servers: * **Brave Search MCP** (`windsor/brave-search-mcp`): For finding current travel information, flight options, hotel reviews, and booking options * **Open Meteo MCP** (`windsor/open-meteo-mcp`): For weather forecasts at your destination * **Exa MCP** (`tsion/exa`): For finding concerts and events during your trip Try these servers out in your projects! # Weather Forecaster Source: https://docs.dedaluslabs.ai/sdk/agents/use-cases/weather-forecaster Detailed weather analysis with recommendations Weather APIs return data. Users want recommendations. An agent with access to weather data can translate forecasts into actionable advice for specific situations. ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio from dedalus_labs import AsyncDedalus, DedalusRunner from dotenv import load_dotenv load_dotenv() async def main(): client = AsyncDedalus() runner = DedalusRunner(client) result = await runner.run( input="""I'm planning an outdoor wedding in San Francisco next weekend. Please provide: 1. Current weather conditions 2. 7-day forecast with daily details 3. Precipitation probability 4. Temperature highs and lows 5. Wind and UV conditions 6. Specific recommendations for outdoor event planning""", model="openai/gpt-4.1", mcp_servers=["windsor/open-meteo-mcp"] ) print(result.final_output) if __name__ == "__main__": asyncio.run(main()) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus, { DedalusRunner } from "dedalus-labs"; import * as dotenv from "dotenv"; dotenv.config(); async function main() { const client = new Dedalus({ apiKey: process.env.DEDALUS_API_KEY, }); const runner = new DedalusRunner(client); const result = await runner.run({ input: `I'm planning an outdoor wedding in San Francisco next weekend. Please provide: 1. Current weather conditions 2. 7-day forecast with daily details 3. Precipitation probability 4. Temperature highs and lows 5. Wind and UV conditions 6. Specific recommendations for outdoor event planning`, model: "openai/gpt-4.1", mcpServers: ["windsor/open-meteo-mcp"], }); console.log(result.finalOutput); } main(); ``` ## Open Meteo Capabilities The `windsor/open-meteo-mcp` server provides: * Current conditions * Multi-day forecasts (hourly and daily) * Historical weather data * Weather alerts * Global coverage (no API key required) ## Beyond Raw Data Any API can fetch weather. The agent interprets it: wind affecting outdoor events, rain probability suggesting backup plans, UV levels for guest safety, temperature changes through the day. Same pattern applies to any data-to-advice task. Health metrics become fitness recommendations. Market data becomes investment suggestions. Sensor readings become maintenance alerts. # Web Search Agent Source: https://docs.dedaluslabs.ai/sdk/agents/use-cases/web-search-agent Create a web search agent using multiple search MCPs to find and analyze information from the web. ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio from dedalus_labs import AsyncDedalus, DedalusRunner from dotenv import load_dotenv load_dotenv() async def main(): client = AsyncDedalus() runner = DedalusRunner(client) result = await runner.run( input="""I need to research the latest developments in AI agents for 2024. Please help me: 1. Find recent news articles about AI agent breakthroughs 2. Search for academic papers on multi-agent systems 3. Look up startup companies working on AI agents 4. Find GitHub repositories with popular agent frameworks 5. Summarize the key trends and provide relevant links Focus on developments from the past 6 months.""", model="openai/gpt-4.1", mcp_servers=[ "tsion/exa", # Semantic search engine "windsor/brave-search-mcp" # Privacy-focused web search ] ) print(f"Web Search Results:\n{result.final_output}") if __name__ == "__main__": asyncio.run(main()) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus, { DedalusRunner } from "dedalus-labs"; import * as dotenv from "dotenv"; dotenv.config(); async function main() { const client = new Dedalus({ apiKey: process.env.DEDALUS_API_KEY, }); const runner = new DedalusRunner(client); const result = await runner.run({ input: `I need to research the latest developments in AI agents for 2024. Please help me: 1. Find recent news articles about AI agent breakthroughs 2. Search for academic papers on multi-agent systems 3. Look up startup companies working on AI agents 4. Find GitHub repositories with popular agent frameworks 5. Summarize the key trends and provide relevant links Focus on developments from the past 6 months.`, model: "openai/gpt-4.1", mcpServers: [ "tsion/exa", // Semantic search engine "windsor/brave-search-mcp", // Privacy-focused web search ], }); console.log(`Web Search Results:\n${result.finalOutput}`); } main(); ``` This example uses multiple search MCP servers: * **Exa MCP** (`tsion/exa`): Semantic search, great for finding conceptually related content * **Brave Search MCP** (`windsor/brave-search-mcp`): Privacy-focused web search for current events and specific queries Together, they cover more ground than either alone—Exa finds related ideas while Brave handles current events. # DCS Go SDK Source: https://docs.dedaluslabs.ai/sdk/dcs/go v0.4.0 ## Installation ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} go get github.com/dedalus-labs/dedalus-go ``` This library requires Go 1.22+. ## Usage See the full method reference in the [API Reference](/api-reference/dcs) tab. ```go theme={"theme":{"light":"github-light","dark":"github-dark"}} package main import ( "context" "fmt" "github.com/dedalus-labs/dedalus-go" "github.com/dedalus-labs/dedalus-go/option" ) func main() { client := dedalus.NewClient( option.WithAPIKey("My API Key"), // defaults to os.LookupEnv("DEDALUS_API_KEY") ) machine, err := client.Machines.New(context.TODO(), dedalus.MachineNewParams{ CreateParams: dedalus.CreateParams{ MemoryMiB: 4096, StorageGiB: 10, VCPU: 1, }, }) if err != nil { panic(err.Error()) } fmt.Printf("%+v\n", machine.MachineID) } ``` ### Request fields The dedalus library uses the [`omitzero`](https://tip.golang.org/doc/go1.24#encodingjsonpkgencodingjson) semantics from the Go 1.24+ `encoding/json` release for request fields. Required primitive fields (`int64`, `string`, etc.) feature the tag \`api:"required"\`. These fields are always serialized, even their zero values. Optional primitive types are wrapped in a `param.Opt[T]`. These fields can be set with the provided constructors, `dedalus.String(string)`, `dedalus.Int(int64)`, etc. Any `param.Opt[T]`, map, slice, struct or string enum uses the tag \`json:"...,omitzero"\`. Its zero value is considered omitted. The `param.IsOmitted(any)` function can confirm the presence of any `omitzero` field. ```go theme={"theme":{"light":"github-light","dark":"github-dark"}} p := dedalus.ExampleParams{ ID: "id_xxx", // required property Name: dedalus.String("..."), // optional property Point: dedalus.Point{ X: 0, // required field will serialize as 0 Y: dedalus.Int(1), // optional field will serialize as 1 // ... omitted non-required fields will not be serialized }, Origin: dedalus.Origin{}, // the zero value of [Origin] is considered omitted } ``` To send `null` instead of a `param.Opt[T]`, use `param.Null[T]()`. To send `null` instead of a struct `T`, use `param.NullStruct[T]()`. ```go theme={"theme":{"light":"github-light","dark":"github-dark"}} p.Name = param.Null[string]() // 'null' instead of string p.Point = param.NullStruct[Point]() // 'null' instead of struct param.IsNull(p.Name) // true param.IsNull(p.Point) // true ``` Request structs contain a `.SetExtraFields(map[string]any)` method which can send non-conforming fields in the request body. Extra fields overwrite any struct fields with a matching key. For security reasons, only use `SetExtraFields` with trusted data. To send a custom value instead of a struct, use `param.Override[T](value)`. ```go theme={"theme":{"light":"github-light","dark":"github-dark"}} // In cases where the API specifies a given type, // but you want to send something else, use [SetExtraFields]: p.SetExtraFields(map[string]any{ "x": 0.01, // send "x" as a float instead of int }) // Send a number instead of an object custom := param.Override[dedalus.FooParams](12) ``` ### Request unions Unions are represented as a struct with fields prefixed by "Of" for each of its variants, only one field can be non-zero. The non-zero field will be serialized. Sub-properties of the union can be accessed via methods on the union struct. These methods return a mutable pointer to the underlying data, if present. ```go theme={"theme":{"light":"github-light","dark":"github-dark"}} // Only one field can be non-zero, use param.IsOmitted() to check if a field is set type AnimalUnionParam struct { OfCat *Cat `json:",omitzero,inline` OfDog *Dog `json:",omitzero,inline` } animal := AnimalUnionParam{ OfCat: &Cat{ Name: "Whiskers", Owner: PersonParam{ Address: AddressParam{Street: "3333 Coyote Hill Rd", Zip: 0}, }, }, } // Mutating a field if address := animal.GetOwner().GetAddress(); address != nil { address.ZipCode = 94304 } ``` ### Response objects All fields in response structs are ordinary value types (not pointers or wrappers). Response structs also include a special `JSON` field containing metadata about each property. ```go theme={"theme":{"light":"github-light","dark":"github-dark"}} type Animal struct { Name string `json:"name,nullable"` Owners int `json:"owners"` Age int `json:"age"` JSON struct { Name respjson.Field Owner respjson.Field Age respjson.Field ExtraFields map[string]respjson.Field } `json:"-"` } ``` To handle optional data, use the `.Valid()` method on the JSON field. `.Valid()` returns true if a field is not `null`, not present, or couldn't be marshaled. If `.Valid()` is false, the corresponding field will simply be its zero value. ```go theme={"theme":{"light":"github-light","dark":"github-dark"}} raw := `{"owners": 1, "name": null}` var res Animal json.Unmarshal([]byte(raw), &res) // Accessing regular fields res.Owners // 1 res.Name // "" res.Age // 0 // Optional field checks res.JSON.Owners.Valid() // true res.JSON.Name.Valid() // false res.JSON.Age.Valid() // false // Raw JSON values res.JSON.Owners.Raw() // "1" res.JSON.Name.Raw() == "null" // true res.JSON.Name.Raw() == respjson.Null // true res.JSON.Age.Raw() == "" // true res.JSON.Age.Raw() == respjson.Omitted // true ``` These `.JSON` structs also include an `ExtraFields` map containing any properties in the json response that were not specified in the struct. This can be useful for API features not yet present in the SDK. ```go theme={"theme":{"light":"github-light","dark":"github-dark"}} body := res.JSON.ExtraFields["my_unexpected_field"].Raw() ``` ### Response Unions In responses, unions are represented by a flattened struct containing all possible fields from each of the object variants. To convert it to a variant use the `.AsFooVariant()` method or the `.AsAny()` method if present. If a response value union contains primitive values, primitive fields will be alongside the properties but prefixed with `Of` and feature the tag `json:"...,inline"`. ```go theme={"theme":{"light":"github-light","dark":"github-dark"}} type AnimalUnion struct { // From variants [Dog], [Cat] Owner Person `json:"owner"` // From variant [Dog] DogBreed string `json:"dog_breed"` // From variant [Cat] CatBreed string `json:"cat_breed"` // ... JSON struct { Owner respjson.Field // ... } `json:"-"` } // If animal variant if animal.Owner.Address.ZipCode == "" { panic("missing zip code") } // Switch on the variant switch variant := animal.AsAny().(type) { case Dog: case Cat: default: panic("unexpected type") } ``` ### RequestOptions This library uses the functional options pattern. Functions defined in the `option` package return a `RequestOption`, which is a closure that mutates a `RequestConfig`. These options can be supplied to the client or at individual requests. For example: ```go theme={"theme":{"light":"github-light","dark":"github-dark"}} client := dedalus.NewClient( // Adds a header to every request made by the client option.WithHeader("X-Some-Header", "custom_header_info"), ) client.Machines.New(context.TODO(), dedalus.MachineNewParams{ CreateParams: dedalus.CreateParams{ MemoryMiB: 4096, StorageGiB: 10, VCPU: 1, }, }, // Override the header option.WithHeader("X-Some-Header", "some_other_custom_header_info"), // Add an undocumented field to the request body, using sjson syntax option.WithJSONSet("some.json.path", map[string]string{"my": "object"}), ) ``` The request option `option.WithDebugLog(nil)` may be helpful while debugging. See the [full list of request options](https://pkg.go.dev/github.com/dedalus-labs/dedalus-go/option). ### Pagination This library provides some conveniences for working with paginated list endpoints. You can use `.ListAutoPaging()` methods to iterate through items across all pages: ```go theme={"theme":{"light":"github-light","dark":"github-dark"}} iter := client.Machines.ListAutoPaging(context.TODO(), dedalus.MachineListParams{}) // Automatically fetches more pages as needed. for iter.Next() { machineListItem := iter.Current() fmt.Printf("%+v\n", machineListItem) } if err := iter.Err(); err != nil { panic(err.Error()) } ``` Or you can use simple `.List()` methods to fetch a single page and receive a standard response object with additional helper methods like `.GetNextPage()`, e.g.: ```go theme={"theme":{"light":"github-light","dark":"github-dark"}} page, err := client.Machines.List(context.TODO(), dedalus.MachineListParams{}) for page != nil { for _, machine := range page.Items { fmt.Printf("%+v\n", machine) } page, err = page.GetNextPage() } if err != nil { panic(err.Error()) } ``` ### Errors When the API returns a non-success status code, we return an error with type `*dedalus.Error`. This contains the `StatusCode`, `*http.Request`, and `*http.Response` values of the request, as well as the JSON of the error body (much like other response objects in the SDK). To handle errors, we recommend that you use the `errors.As` pattern: ```go theme={"theme":{"light":"github-light","dark":"github-dark"}} _, err := client.Machines.New(context.TODO(), dedalus.MachineNewParams{ CreateParams: dedalus.CreateParams{ MemoryMiB: 4096, StorageGiB: 10, VCPU: 1, }, }) if err != nil { var apierr *dedalus.Error if errors.As(err, &apierr) { println(string(apierr.DumpRequest(true))) // Prints the serialized HTTP request println(string(apierr.DumpResponse(true))) // Prints the serialized HTTP response println(apierr.ErrorCode) // IDEMPOTENCY_KEY_REUSED println(apierr.Message) // idempotency key reused with different request parameters println(apierr.Retryable) // false } panic(err.Error()) // GET "/v1/machines": 400 Bad Request { ... } } ``` When other errors occur, they are returned unwrapped; for example, if HTTP transport fails, you might receive `*url.Error` wrapping `*net.OpError`. ### Timeouts Requests do not time out by default; use context to configure a timeout for a request lifecycle. Note that if a request is [retried](#retries), the context timeout does not start over. To set a per-retry timeout, use `option.WithRequestTimeout()`. ```go theme={"theme":{"light":"github-light","dark":"github-dark"}} // This sets the timeout for the request, including all the retries. ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) defer cancel() client.Machines.New( ctx, dedalus.MachineNewParams{ CreateParams: dedalus.CreateParams{ MemoryMiB: 4096, StorageGiB: 10, VCPU: 1, }, }, // This sets the per-retry timeout option.WithRequestTimeout(20*time.Second), ) ``` ### File uploads Request parameters that correspond to file uploads in multipart requests are typed as `io.Reader`. The contents of the `io.Reader` will by default be sent as a multipart form part with the file name of "anonymous\_file" and content-type of "application/octet-stream". The file name and content-type can be customized by implementing `Name() string` or `ContentType() string` on the run-time type of `io.Reader`. Note that `os.File` implements `Name() string`, so a file returned by `os.Open` will be sent with the file name on disk. We also provide a helper `dedalus.File(reader io.Reader, filename string, contentType string)` which can be used to wrap any `io.Reader` with the appropriate file name and content type. ### Retries Certain errors will be automatically retried 2 times by default, with a short exponential backoff. We retry by default all connection errors, 408 Request Timeout, 409 Conflict, 429 Rate Limit, and >=500 Internal errors. You can use the `WithMaxRetries` option to configure or disable this: ```go theme={"theme":{"light":"github-light","dark":"github-dark"}} // Configure the default for all requests: client := dedalus.NewClient( option.WithMaxRetries(0), // default is 2 ) // Override per-request: client.Machines.New( context.TODO(), dedalus.MachineNewParams{ CreateParams: dedalus.CreateParams{ MemoryMiB: 4096, StorageGiB: 10, VCPU: 1, }, }, option.WithMaxRetries(5), ) ``` ### Accessing raw response data (e.g. response headers) You can access the raw HTTP response data by using the `option.WithResponseInto()` request option. This is useful when you need to examine response headers, status codes, or other details. ```go theme={"theme":{"light":"github-light","dark":"github-dark"}} // Create a variable to store the HTTP response var response *http.Response machine, err := client.Machines.New( context.TODO(), dedalus.MachineNewParams{ CreateParams: dedalus.CreateParams{ MemoryMiB: 4096, StorageGiB: 10, VCPU: 1, }, }, option.WithResponseInto(&response), ) if err != nil { // handle error } fmt.Printf("%+v\n", machine) fmt.Printf("Status Code: %d\n", response.StatusCode) fmt.Printf("Headers: %+#v\n", response.Header) ``` ### Making custom/undocumented requests This library is typed for convenient access to the documented API. If you need to access undocumented endpoints, params, or response properties, the library can still be used. #### Undocumented endpoints To make requests to undocumented endpoints, you can use `client.Get`, `client.Post`, and other HTTP verbs. `RequestOptions` on the client, such as retries, will be respected when making these requests. ```go theme={"theme":{"light":"github-light","dark":"github-dark"}} var ( // params can be an io.Reader, a []byte, an encoding/json serializable object, // or a "…Params" struct defined in this library. params map[string]any // result can be an []byte, *http.Response, a encoding/json deserializable object, // or a model defined in this library. result *http.Response ) err := client.Post(context.Background(), "/unspecified", params, &result) if err != nil { … } ``` #### Undocumented request params To make requests using undocumented parameters, you may use either the `option.WithQuerySet()` or the `option.WithJSONSet()` methods. ```go theme={"theme":{"light":"github-light","dark":"github-dark"}} params := FooNewParams{ ID: "id_xxxx", Data: FooNewParamsData{ FirstName: dedalus.String("John"), }, } client.Foo.New(context.Background(), params, option.WithJSONSet("data.last_name", "Doe")) ``` #### Undocumented response properties To access undocumented response properties, you may either access the raw JSON of the response as a string with `result.JSON.RawJSON()`, or get the raw JSON of a particular field on the result with `result.JSON.Foo.Raw()`. Any fields that are not present on the response struct will be saved and can be accessed by `result.JSON.ExtraFields()` which returns the extra fields as a `map[string]Field`. ### Middleware We provide `option.WithMiddleware` which applies the given middleware to requests. ```go theme={"theme":{"light":"github-light","dark":"github-dark"}} func Logger(req *http.Request, next option.MiddlewareNext) (res *http.Response, err error) { // Before the request start := time.Now() LogReq(req) // Forward the request to the next handler res, err = next(req) // Handle stuff after the request end := time.Now() LogRes(res, err, start - end) return res, err } client := dedalus.NewClient( option.WithMiddleware(Logger), ) ``` When multiple middlewares are provided as variadic arguments, the middlewares are applied left to right. If `option.WithMiddleware` is given multiple times, for example first in the client then the method, the middleware in the client will run first and the middleware given in the method will run next. You may also replace the default `http.Client` with `option.WithHTTPClient(client)`. Only one http client is accepted (this overwrites any previous client) and receives requests after any middleware has been applied. ## Semantic versioning This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions: 1. Changes to library internals which are technically public but not intended or documented for external use. *(Please open a GitHub issue to let us know if you are relying on such internals.)* 2. Changes that we do not expect to impact the vast majority of users in practice. We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience. We are keen for your feedback; please open an [issue](https://www.github.com/dedalus-labs/dedalus-go/issues) with questions, bugs, or suggestions. # DCS Python SDK Source: https://docs.dedaluslabs.ai/sdk/dcs/python v0.4.0 ## Installation ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} pip install dedalus-sdk ``` ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} uv add dedalus-sdk ``` ## Usage See the full method reference in the [API Reference](/api-reference/dcs) tab. ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} import os from dedalus_sdk import Dedalus client = Dedalus( api_key=os.environ.get("DEDALUS_API_KEY"), # This is the default and can be omitted ) machine = client.machines.create( memory_mib=4096, storage_gib=10, vcpu=1, ) print(machine.machine_id) ``` While you can provide a `x_api_key` keyword argument, we recommend using [python-dotenv](https://pypi.org/project/python-dotenv/) to add `DEDALUS_X_API_KEY="My X API Key"` to your `.env` file so that your X API Key is not stored in source control. ## Async Usage Simply import `AsyncDedalus` instead of `Dedalus` and use `await` with each API call: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} import os import asyncio from dedalus_sdk import AsyncDedalus client = AsyncDedalus( api_key=os.environ.get("DEDALUS_API_KEY"), # This is the default and can be omitted ) async def main() -> None: machine = await client.machines.create( memory_mib=4096, storage_gib=10, vcpu=1, ) print(machine.machine_id) asyncio.run(main()) ``` Functionality between the synchronous and asynchronous clients is otherwise identical. ### With aiohttp By default, the async client uses `httpx` for HTTP requests. However, for improved concurrency performance you may also use `aiohttp` as the HTTP backend. You can enable this by installing `aiohttp`: ```sh theme={"theme":{"light":"github-light","dark":"github-dark"}} # install from PyPI pip install dedalus-sdk[aiohttp] ``` Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} import os import asyncio from dedalus_sdk import DefaultAioHttpClient from dedalus_sdk import AsyncDedalus async def main() -> None: async with AsyncDedalus( api_key=os.environ.get("DEDALUS_API_KEY"), # This is the default and can be omitted http_client=DefaultAioHttpClient(), ) as client: machine = await client.machines.create( memory_mib=4096, storage_gib=10, vcpu=1, ) print(machine.machine_id) asyncio.run(main()) ``` ## Streaming We provide support for streaming responses using Server Side Events (SSE). ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_sdk import Dedalus client = Dedalus() stream = client.machines.watch( machine_id="machine_id", ) for machine in stream: print(machine.machine_id) ``` The async client uses the exact same interface. ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_sdk import AsyncDedalus client = AsyncDedalus() stream = await client.machines.watch( machine_id="machine_id", ) async for machine in stream: print(machine.machine_id) ``` ## Using types Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typing.html#typing.TypedDict). Responses are [Pydantic models](https://docs.pydantic.dev) which also provide helper methods for things like: * Serializing back into JSON, `model.to_json()` * Converting to a dictionary, `model.to_dict()` Typed requests and responses provide autocomplete and documentation within your editor. If you would like to see type errors in VS Code to help catch bugs earlier, set `python.analysis.typeCheckingMode` to `basic`. ## Pagination List methods in the Dedalus API are paginated. This library provides auto-paginating iterators with each list response, so you do not have to request successive pages manually: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_sdk import Dedalus client = Dedalus() all_machines = [] # Automatically fetches more pages as needed. for machine in client.machines.list(): # Do something with machine here all_machines.append(machine) print(all_machines) ``` Or, asynchronously: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio from dedalus_sdk import AsyncDedalus client = AsyncDedalus() async def main() -> None: all_machines = [] # Iterate through items across all pages, issuing requests as needed. async for machine in client.machines.list(): all_machines.append(machine) print(all_machines) asyncio.run(main()) ``` Alternatively, you can use the `.has_next_page()`, `.next_page_info()`, or `.get_next_page()` methods for more granular control working with pages: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} first_page = await client.machines.list() if first_page.has_next_page(): print(f"will fetch next page using these details: {first_page.next_page_info()}") next_page = await first_page.get_next_page() print(f"number of items we just fetched: {len(next_page.items)}") # Remove `await` for non-async usage. ``` Or just work directly with the returned data: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} first_page = await client.machines.list() print(f"next page cursor: {first_page.next_cursor}") # => "next page cursor: ..." for machine in first_page.items: print(machine.machine_id) # Remove `await` for non-async usage. ``` ## Error Handling Always wrap API calls in try/catch. The SDK throws typed errors for HTTP failures. When the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `dedalus_sdk.APIConnectionError` is raised. When the API returns a non-success status code (that is, 4xx or 5xx response), a subclass of `dedalus_sdk.APIStatusError` is raised, containing `status_code` and `response` properties. All errors inherit from `dedalus_sdk.APIError`. ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} import dedalus_sdk from dedalus_sdk import Dedalus client = Dedalus() try: client.machines.create( memory_mib=4096, storage_gib=10, vcpu=1, ) except dedalus_sdk.APIConnectionError as e: print("The server could not be reached") print(e.__cause__) # an underlying Exception, likely raised within httpx. except dedalus_sdk.RateLimitError as e: print("A 429 status code was received; we should back off a bit.") except dedalus_sdk.APIStatusError as e: print("Another non-200-range status code was received") print(e.status_code) print(e.response) ``` Error codes are as follows: | Status Code | Error Type | | ----------- | -------------------------- | | 400 | `BadRequestError` | | 401 | `AuthenticationError` | | 403 | `PermissionDeniedError` | | 404 | `NotFoundError` | | 422 | `UnprocessableEntityError` | | 429 | `RateLimitError` | | >=500 | `InternalServerError` | | N/A | `APIConnectionError` | ### Retries Certain errors are automatically retried 2 times by default, with a short exponential backoff. Connection errors (for example, due to a network connectivity problem), 408 Request Timeout, 409 Conflict, 429 Rate Limit, and >=500 Internal errors are all retried by default. You can use the `max_retries` option to configure or disable retry settings: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_sdk import Dedalus # Configure the default for all requests: client = Dedalus( # default is 2 max_retries=0, ) # Or, configure per-request: client.with_options(max_retries=5).machines.create( memory_mib=4096, storage_gib=10, vcpu=1, ) ``` ### Timeouts By default requests time out after 1 minute. You can configure this with a `timeout` option, which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/timeouts/#fine-tuning-the-configuration) object: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_sdk import Dedalus # Configure the default for all requests: client = Dedalus( # 20 seconds (default is 1 minute) timeout=20.0, ) # More granular control: client = Dedalus( timeout=httpx.Timeout(60.0, read=5.0, write=10.0, connect=2.0), ) # Override per-request: client.with_options(timeout=5.0).machines.create( memory_mib=4096, storage_gib=10, vcpu=1, ) ``` On timeout, an `APITimeoutError` is thrown. Note that requests that time out are [retried twice by default](#retries). ### Logging We use the standard library [`logging`](https://docs.python.org/3/library/logging.html) module. You can enable logging by setting the environment variable `DEDALUS_LOG` to `info`. ```shell theme={"theme":{"light":"github-light","dark":"github-dark"}} $ export DEDALUS_LOG=info ``` Or to `debug` for more verbose logging. ### How to tell whether `None` means `null` or missing In an API response, a field may be explicitly `null`, or missing entirely; in either case, its value is `None` in this library. You can differentiate the two cases with `.model_fields_set`: ```py theme={"theme":{"light":"github-light","dark":"github-dark"}} if response.my_field is None: if 'my_field' not in response.model_fields_set: print('Got json like {}, without a "my_field" key present at all.') else: print('Got json like {"my_field": null}.') ``` ### Accessing raw response data (e.g. headers) The "raw" Response object can be accessed by prefixing `.with_raw_response.` to any HTTP method call, e.g., ```py theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_sdk import Dedalus client = Dedalus() response = client.machines.with_raw_response.create( memory_mib=4096, storage_gib=10, vcpu=1, ) print(response.headers.get('X-My-Header')) machine = response.parse() # get the object that `machines.create()` would have returned print(machine.machine_id) ``` These methods return an [`APIResponse`](https://github.com/dedalus-labs/dedalus-python/tree/main/src/dedalus_sdk/_response.py) object. The async client returns an [`AsyncAPIResponse`](https://github.com/dedalus-labs/dedalus-python/tree/main/src/dedalus_sdk/_response.py) with the same structure, the only difference being `await`able methods for reading the response content. #### `.with_streaming_response` The above interface eagerly reads the full response body when you make the request, which may not always be what you want. To stream the response body, use `.with_streaming_response` instead, which requires a context manager and only reads the response body once you call `.read()`, `.text()`, `.json()`, `.iter_bytes()`, `.iter_text()`, `.iter_lines()` or `.parse()`. In the async client, these are async methods. ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} with client.machines.with_streaming_response.create( memory_mib=4096, storage_gib=10, vcpu=1, ) as response: print(response.headers.get("X-My-Header")) for line in response.iter_lines(): print(line) ``` The context manager is required so that the response will reliably be closed. ### Making custom/undocumented requests This library is typed for convenient access to the documented API. If you need to access undocumented endpoints, params, or response properties, the library can still be used. #### Undocumented endpoints To make requests to undocumented endpoints, you can make requests using `client.get`, `client.post`, and other http verbs. Options on the client will be respected (such as retries) when making this request. ```py theme={"theme":{"light":"github-light","dark":"github-dark"}} import httpx response = client.post( "/foo", cast_to=httpx.Response, body={"my_param": True}, ) print(response.headers.get("x-foo")) ``` #### Undocumented request params If you want to explicitly send an extra param, you can do so with the `extra_query`, `extra_body`, and `extra_headers` request options. #### Undocumented response properties To access undocumented response properties, you can access the extra fields like `response.unknown_prop`. You can also get all the extra fields on the Pydantic model as a dict with [`response.model_extra`](https://docs.pydantic.dev/latest/api/base_model/#pydantic.BaseModel.model_extra). ### Configuring the HTTP client You can directly override the [httpx client](https://www.python-httpx.org/api/#client) to customize it for your use case, including: * Support for [proxies](https://www.python-httpx.org/advanced/proxies/) * Custom [transports](https://www.python-httpx.org/advanced/transports/) * Additional [advanced](https://www.python-httpx.org/advanced/clients/) functionality ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} import httpx from dedalus_sdk import Dedalus, DefaultHttpxClient client = Dedalus( # Or use the `DEDALUS_BASE_URL` env var base_url="http://my.test.server.example.com:8083", http_client=DefaultHttpxClient( proxy="http://my.test.proxy.example.com", transport=httpx.HTTPTransport(local_address="0.0.0.0"), ), ) ``` You can also customize the client on a per-request basis by using `with_options()`: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} client.with_options(http_client=DefaultHttpxClient(...)) ``` ### Managing HTTP resources By default the library closes underlying HTTP connections whenever the client is [garbage collected](https://docs.python.org/3/reference/datamodel.html#object.__del__). You can manually close the client using the `.close()` method if desired, or with a context manager that closes when exiting. ```py theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_sdk import Dedalus with Dedalus() as client: # make requests here ... # HTTP client is now closed ``` ## Versioning This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions: 1. Changes that only affect static types, without breaking runtime behavior. 2. Changes to library internals which are technically public but not intended or documented for external use. *(Please open a GitHub issue to let us know if you are relying on such internals.)* 3. Changes that we do not expect to impact the vast majority of users in practice. We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience. We are keen for your feedback; please open an [issue](https://www.github.com/dedalus-labs/dedalus-python/issues) with questions, bugs, or suggestions. ### Determining the installed version If you've upgraded to the latest version but aren't seeing any new features you were expecting then your python environment is likely still using an older version. You can determine the version that is being used at runtime with: ```py theme={"theme":{"light":"github-light","dark":"github-dark"}} import dedalus_sdk print(dedalus_sdk.__version__) ``` Python 3.9 or higher. # DCS TypeScript SDK Source: https://docs.dedaluslabs.ai/sdk/dcs/typescript v0.4.0 ## Installation ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} npm install dedalus ``` ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} pnpm add dedalus ``` ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} yarn add dedalus ``` ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} bun add dedalus ``` ## Usage See the full method reference in the [API Reference](/api-reference/dcs) tab. ```js theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from 'dedalus'; const client = new Dedalus({ apiKey: process.env['DEDALUS_API_KEY'], // This is the default and can be omitted }); const machine = await client.machines.create({ memory_mib: 4096, storage_gib: 10, vcpu: 1, }); console.log(machine.machine_id); ``` ## Streaming We provide support for streaming responses using Server Sent Events (SSE). ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from 'dedalus'; const client = new Dedalus(); const stream = await client.machines.watch({ machine_id: 'machine_id' }); for await (const machine of stream) { console.log(machine.machine_id); } ``` If you need to cancel a stream, you can `break` from the loop or call `stream.controller.abort()`. ### Request & Response types This library includes TypeScript definitions for all request params and response fields. You may import and use them like so: ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from 'dedalus'; const client = new Dedalus({ apiKey: process.env['DEDALUS_API_KEY'], // This is the default and can be omitted }); const params: Dedalus.MachineCreateParams = { memory_mib: 4096, storage_gib: 10, vcpu: 1, }; const machine: Dedalus.Machine = await client.machines.create(params); ``` Documentation for each method, request param, and response field are available in docstrings and will appear on hover in most modern editors. ## Error Handling Always wrap API calls in try/catch. The SDK throws typed errors for HTTP failures. When the library is unable to connect to the API, or if the API returns a non-success status code (i.e., 4xx or 5xx response), a subclass of `APIError` will be thrown: ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}} const machine = await client.machines .create({ memory_mib: 4096, storage_gib: 10, vcpu: 1, }) .catch(async (err) => { if (err instanceof Dedalus.APIError) { console.log(err.status); // 400 console.log(err.name); // BadRequestError console.log(err.error?.error_code); // IDEMPOTENCY_KEY_REUSED console.log(err.error?.message); // idempotency key reused with different request parameters console.log(err.error?.retryable); // false console.log(err.headers); // {server: 'nginx', ...} } else { throw err; } }); ``` Error codes are as follows: | Status Code | Error Type | | ----------- | -------------------------- | | 400 | `BadRequestError` | | 401 | `AuthenticationError` | | 403 | `PermissionDeniedError` | | 404 | `NotFoundError` | | 422 | `UnprocessableEntityError` | | 429 | `RateLimitError` | | >=500 | `InternalServerError` | | N/A | `APIConnectionError` | ### Retries Certain errors will be automatically retried 2 times by default, with a short exponential backoff. Connection errors (for example, due to a network connectivity problem), 408 Request Timeout, 409 Conflict, 429 Rate Limit, and >=500 Internal errors will all be retried by default. You can use the `maxRetries` option to configure or disable this: ```js theme={"theme":{"light":"github-light","dark":"github-dark"}} // Configure the default for all requests: const client = new Dedalus({ maxRetries: 0, // default is 2 }); // Or, configure per-request: await client.machines.create({ memory_mib: 4096, storage_gib: 10, vcpu: 1, }, { maxRetries: 5, }); ``` ### Timeouts Requests time out after 1 minute by default. You can configure this with a `timeout` option: ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}} // Configure the default for all requests: const client = new Dedalus({ timeout: 20 * 1000, // 20 seconds (default is 1 minute) }); // Override per-request: await client.machines.create({ memory_mib: 4096, storage_gib: 10, vcpu: 1, }, { timeout: 5 * 1000, }); ``` On timeout, an `APIConnectionTimeoutError` is thrown. Note that requests which time out will be [retried twice by default](#retries). ## Pagination List methods in the Dedalus API are paginated. You can use the `for await … of` syntax to iterate through items across all pages: ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}} async function fetchAllMachineListItems(params) { const allMachineListItems = []; // Automatically fetches more pages as needed. for await (const machineListItem of client.machines.list()) { allMachineListItems.push(machineListItem); } return allMachineListItems; } ``` Alternatively, you can request a single page at a time: ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}} let page = await client.machines.list(); for (const machineListItem of page.items) { console.log(machineListItem); } // Convenience methods are provided for manually paginating: while (page.hasNextPage()) { page = await page.getNextPage(); // ... } ``` ### Accessing raw Response data (e.g., headers) The "raw" `Response` returned by `fetch()` can be accessed through the `.asResponse()` method on the `APIPromise` type that all methods return. This method returns as soon as the headers for a successful response are received and does not consume the response body, so you are free to write custom parsing or streaming logic. You can also use the `.withResponse()` method to get the raw `Response` along with the parsed data. Unlike `.asResponse()` this method consumes the body, returning once it is parsed. ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}} const client = new Dedalus(); const response = await client.machines .create({ memory_mib: 4096, storage_gib: 10, vcpu: 1, }) .asResponse(); console.log(response.headers.get('X-My-Header')); console.log(response.statusText); // access the underlying Response object const { data: machine, response: raw } = await client.machines .create({ memory_mib: 4096, storage_gib: 10, vcpu: 1, }) .withResponse(); console.log(raw.headers.get('X-My-Header')); console.log(machine.machine_id); ``` ### Logging > \[!IMPORTANT] > All log messages are intended for debugging only. The format and content of log messages > may change between releases. #### Log levels The log level can be configured in two ways: 1. Via the `DEDALUS_LOG` environment variable 2. Using the `logLevel` client option (overrides the environment variable if set) ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from 'dedalus'; const client = new Dedalus({ logLevel: 'debug', // Show all log messages }); ``` Available log levels, from most to least verbose: * `'debug'` - Show debug messages, info, warnings, and errors * `'info'` - Show info messages, warnings, and errors * `'warn'` - Show warnings and errors (default) * `'error'` - Show only errors * `'off'` - Disable all logging At the `'debug'` level, all HTTP requests and responses are logged, including headers and bodies. Some authentication-related headers are redacted, but sensitive data in request and response bodies may still be visible. #### Custom logger By default, this library logs to `globalThis.console`. You can also provide a custom logger. Most logging libraries are supported, including [pino](https://www.npmjs.com/package/pino), [winston](https://www.npmjs.com/package/winston), [bunyan](https://www.npmjs.com/package/bunyan), [consola](https://www.npmjs.com/package/consola), [signale](https://www.npmjs.com/package/signale), and [@std/log](https://jsr.io/@std/log). If your logger doesn't work, please open an issue. When providing a custom logger, the `logLevel` option still controls which messages are emitted, messages below the configured level will not be sent to your logger. ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from 'dedalus'; import pino from 'pino'; const logger = pino(); const client = new Dedalus({ logger: logger.child({ name: 'Dedalus' }), logLevel: 'debug', // Send all messages to pino, allowing it to filter }); ``` ### Making custom/undocumented requests This library is typed for convenient access to the documented API. If you need to access undocumented endpoints, params, or response properties, the library can still be used. #### Undocumented endpoints To make requests to undocumented endpoints, you can use `client.get`, `client.post`, and other HTTP verbs. Options on the client, such as retries, will be respected when making these requests. ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}} await client.post('/some/path', { body: { some_prop: 'foo' }, query: { some_query_arg: 'bar' }, }); ``` #### Undocumented request params To make requests using undocumented parameters, you may use `// @ts-expect-error` on the undocumented parameter. This library doesn't validate at runtime that the request matches the type, so any extra values you send will be sent as-is. ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}} client.machines.create({ memory_mib: 4096, storage_gib: 10, vcpu: 1, // @ts-expect-error baz is not yet public baz: 'undocumented option', }); ``` For requests with the `GET` verb, any extra params will be in the query, all other requests will send the extra param in the body. If you want to explicitly send an extra argument, you can do so with the `query`, `body`, and `headers` request options. #### Undocumented response properties To access undocumented response properties, you may access the response object with `// @ts-expect-error` on the response object, or cast the response object to the requisite type. Like the request params, we do not validate or strip extra properties from the response from the API. ### Customizing the fetch client By default, this library expects a global `fetch` function is defined. If you want to use a different `fetch` function, you can either polyfill the global: ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}} import fetch from 'my-fetch'; globalThis.fetch = fetch; ``` Or pass it to the client: ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from 'dedalus'; import fetch from 'my-fetch'; const client = new Dedalus({ fetch }); ``` ### Fetch options If you want to set custom `fetch` options without overriding the `fetch` function, you can provide a `fetchOptions` object when instantiating the client or making a request. (Request-specific options override client options.) ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from 'dedalus'; const client = new Dedalus({ fetchOptions: { // `RequestInit` options }, }); ``` #### Configuring proxies To modify proxy behavior, you can provide custom `fetchOptions` that add runtime-specific proxy options to requests: **Node** \[[docs](https://github.com/nodejs/undici/blob/main/docs/docs/api/ProxyAgent.md#example---proxyagent-with-fetch)] ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from 'dedalus'; import * as undici from 'undici'; const proxyAgent = new undici.ProxyAgent('http://localhost:8888'); const client = new Dedalus({ fetchOptions: { dispatcher: proxyAgent, }, }); ``` **Bun** \[[docs](https://bun.sh/guides/http/proxy)] ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from 'dedalus'; const client = new Dedalus({ fetchOptions: { proxy: 'http://localhost:8888', }, }); ``` **Deno** \[[docs](https://docs.deno.com/api/deno/~/Deno.createHttpClient)] ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from 'npm:dedalus'; const httpClient = Deno.createHttpClient({ proxy: { url: 'http://localhost:8888' } }); const client = new Dedalus({ fetchOptions: { client: httpClient, }, }); ``` ## Frequently Asked Questions ## Semantic versioning This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions: 1. Changes that only affect static types, without breaking runtime behavior. 2. Changes to library internals which are technically public but not intended or documented for external use. *(Please open a GitHub issue to let us know if you are relying on such internals.)* 3. Changes that we do not expect to impact the vast majority of users in practice. We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience. We are keen for your feedback; please open an [issue](https://www.github.com/dedalus-labs/dedalus-typescript/issues) with questions, bugs, or suggestions. TypeScript >= 4.9 is supported. The following runtimes are supported: * Web browsers (Up-to-date Chrome, Firefox, Safari, Edge, and more) * Node.js 20 LTS or later ([non-EOL](https://endoflife.date/nodejs)) versions. * Deno v1.28.0 or higher. * Bun 1.0 or later. * Cloudflare Workers. * Vercel Edge Runtime. * Jest 28 or greater with the `"node"` environment (`"jsdom"` is not supported at this time). * Nitro v2.6 or greater. Note that React Native is not supported at this time. If you are interested in other runtime environments, please open or upvote an issue on GitHub. # Getting Started Source: https://docs.dedaluslabs.ai/sdk/mcp/python/authorization Protect MCP servers with DAuth or external OAuth MCP servers can require OAuth 2.1 tokens. Choose between **DAuth** (Dedalus Auth) for managed authentication with credential isolation, or bring your own authorization server. ## DAuth (Dedalus Auth) DAuth is Dedalus's managed authorization system. It provides OAuth 2.1 token issuance with a key security property: **credentials never leave a sealed execution boundary**. ### Why DAuth? Traditional credential handling exposes secrets to your application code. DAuth isolates credentials in a secure enclave—your MCP server receives an opaque connection handle, not raw API keys. * **Credentials never exposed** — Encrypted client-side, decrypted only in a sealed execution boundary * **Opaque handles** — Your code references connections by handle, never sees raw secrets * **Sender-constrained tokens** — Tokens are cryptographically bound to the client; stolen tokens are unusable * **Networkless execution** — Credential decryption and API calls happen entirely within an isolated enclave; raw secrets never traverse the network Learn how credential isolation and sealed execution protect your secrets. ### Quick Start ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import MCPServer from dedalus_mcp.server import AuthorizationConfig server = MCPServer( "protected-server", authorization=AuthorizationConfig( enabled=True, required_scopes=["read"], ), ) ``` By default, `authorization_servers` points to `https://as.dedaluslabs.ai` (the DAuth control plane). For a complete working example with GitHub and Supabase integrations: Production-ready server with GitHub and Supabase integrations. Unauthenticated requests get `401` with a `WWW-Authenticate` challenge pointing to the protected resource metadata. ### Server-level Scopes All requests must have these scopes. Scope names are arbitrary strings you define—common patterns are `read`/`write` for general access or `resource:action` (e.g., `files:delete`) for fine-grained control. ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} authorization=AuthorizationConfig( enabled=True, required_scopes=["read", "write"], # Required for all tools ``` ### Per-tool Scopes Gate sensitive tools with additional scope requirements: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import tool @tool(description="List files") def list_files(path: str) -> list[str]: return os.listdir(path) # No extra scopes needed @tool(description="Delete file", required_scopes=["files:delete"]) def delete_file(path: str) -> dict: os.remove(path) return {"deleted": path} ``` A token with `read` can call `list_files`. Calling `delete_file` without `files:delete` returns an error: ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "isError": true, "content": [ { "type": "text", "text": "Tool \"delete_file\" requires scopes: ['files:delete']. Missing: ['files:delete']" } ] } ``` ### Configuration Options ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} AuthorizationConfig( enabled=True, authorization_servers=["https://as.dedaluslabs.ai"], # DAuth (default) required_scopes=["read"], metadata_path="/.well-known/oauth-protected-resource", cache_ttl=300, fail_open=False, ) ``` | Option | Default | Description | | ----------------------- | --------------------------------------- | ------------------------------------ | | `enabled` | `False` | Enable authorization enforcement | | `authorization_servers` | `["https://as.dedaluslabs.ai"]` | DAuth or custom OAuth AS URLs | | `required_scopes` | `[]` | Scopes required for all requests | | `metadata_path` | `/.well-known/oauth-protected-resource` | PRM endpoint path | | `cache_ttl` | `300` | Cache duration for metadata | | `fail_open` | `False` | Allow requests when validation fails | ### Access Claims in Tools Inspect the authenticated user in your tools: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import tool, get_context @tool(description="Get current user") def whoami() -> dict: ctx = get_context() auth = ctx.auth # AuthorizationContext or None if auth is None: return {"user": "anonymous"} return { "subject": auth.subject, "scopes": auth.scopes, "claims": auth.claims, } ``` ### DPoP Support DAuth uses DPoP (Demonstrating Proof-of-Possession) by default. Tokens are cryptographically bound to the client's key—even if a token is stolen, it's useless without the corresponding private key. ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} server = MCPServer( "dpop-server", authorization=AuthorizationConfig( enabled=True, dpop_required=True, ), ) ``` ### Environment variable Remember to add these variable to your environment. DAuth works natively with Dedalus SDK, therefore an API key is needed. Get your API key from the [dashboard](https://www.dedaluslabs.ai/dashboard/api-keys) ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} # Dedalus Platform (for _client.py testing) DEDALUS_API_KEY=dsk-... DEDALUS_API_URL=https://api.dedaluslabs.ai DEDALUS_AS_URL=https://as.dedaluslabs.ai ``` ## External Authorization Servers Use your own OAuth 2.1 provider instead of DAuth. This is useful when integrating with existing identity infrastructure. External authorization servers don't provide the sealed execution model. Your MCP server will handle credentials directly. ### Custom Provider ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp.server import AuthorizationConfig, JWTValidatorConfig, JWTValidator server = MCPServer( "my-server", authorization=AuthorizationConfig( enabled=True, authorization_servers=["https://auth.mycompany.com"], required_scopes=["api:access"], ), ) # Configure JWT validation for your provider jwt_config = JWTValidatorConfig( jwks_uri="https://auth.mycompany.com/.well-known/jwks.json", issuer="https://auth.mycompany.com", audience="https://my-mcp-server.example.com", ) server.set_authorization_provider(JWTValidator(jwt_config)) ``` ### Auth0 ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} jwt_config = JWTValidatorConfig( jwks_uri="https://YOUR_DOMAIN.auth0.com/.well-known/jwks.json", issuer="https://YOUR_DOMAIN.auth0.com/", audience="https://my-mcp-api", ) ``` ### Okta ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} jwt_config = JWTValidatorConfig( jwks_uri="https://YOUR_DOMAIN.okta.com/oauth2/default/v1/keys", issuer="https://YOUR_DOMAIN.okta.com/oauth2/default", audience="api://my-mcp-server", ) ``` ### Keycloak ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} jwt_config = JWTValidatorConfig( jwks_uri="https://keycloak.example.com/realms/myrealm/protocol/openid-connect/certs", issuer="https://keycloak.example.com/realms/myrealm", audience="my-mcp-client", ) ``` *** ## Testing Test authorization with the full server and client: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} import pytest from dedalus_mcp import MCPServer, tool from dedalus_mcp.server import AuthorizationConfig from dedalus_mcp.client import MCPClient, BearerAuth @tool(description="Delete file", required_scopes=["files:delete"]) def delete_file(path: str) -> dict: return {"deleted": path} @pytest.fixture async def protected_server(): server = MCPServer( "test", authorization=AuthorizationConfig(enabled=True, required_scopes=["read"]), ) server.collect(delete_file) task = asyncio.create_task(server.serve()) await asyncio.sleep(0.1) yield server task.cancel() async def test_with_valid_token(protected_server): # Use a test token with required scopes client = await MCPClient.connect( "http://127.0.0.1:8000/mcp", auth=BearerAuth(access_token="test-token-with-scopes") ) result = await client.call_tool("delete_file", {"path": "/tmp/test.txt"}) await client.close() ``` # Bearer Auth Source: https://docs.dedaluslabs.ai/sdk/mcp/python/client/bearer-auth Authenticate with Bearer tokens # Bearer Auth > Authenticate with Bearer tokens Bearer tokens are the simplest way to authenticate to protected APIs and MCP servers. They work well for API keys, service accounts, and other non-interactive workflows. ## With Dedalus SDK (DAuth) If you’re using the Dedalus runner / marketplace, you typically declare a **Connection** schema (what secrets you need) and then bind it to real values at runtime. ### Step 1: Define a connection ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import Connection, SecretKeys x = Connection( name="x", secrets=SecretKeys(token="X_BEARER_TOKEN"), base_url="https://api.x.com", # your API provider base URL auth_header_format="Bearer {api_key}", ) ``` ### Step 2: Bind credentials ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} import os from dedalus_mcp import SecretValues # SecretValues binds actual values to the Connection schema. x_secrets = SecretValues(x, token=os.getenv("X_BEARER_TOKEN", "")) ``` ### Step 3: Pass to your runner ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_labs import AsyncDedalus, DedalusRunner async def main(): client = AsyncDedalus() runner = DedalusRunner(client) response = await runner.run( input="Find trending topics", model="anthropic/claude-sonnet-4-20250514", mcp_servers=["windsor/x-api-mcp"], credentials=[x_secrets], ) ``` ### Step 4: Environment variables ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} # Dedalus credentials DEDALUS_API_KEY=dsk-... DEDALUS_AS_URL=https://as.dedaluslabs.ai # Your external API credentials X_BEARER_TOKEN=AAAA... ``` ## `auth_header_format` reference `auth_header_format` controls how the server formats the `Authorization` header when calling **external APIs** through dispatch. The format string **must include `{api_key}`**. | API | Format | Header sent | | ----------- | ------------------ | -------------------------------- | | X (Twitter) | `Bearer {api_key}` | `Authorization: Bearer AAAA...` | | GitHub | `token {api_key}` | `Authorization: token ghp_...` | | Slack | `Bearer {api_key}` | `Authorization: Bearer xoxb-...` | **When to omit `auth_header_format`:** * APIs that authenticate via **query params** or **request body** * APIs with custom auth mechanisms (not a standard auth header) **How to find the right format:** * Check the external API’s auth docs * Look at the API’s 401 response for hints * Most modern APIs use `Bearer {api_key}` ## When to use **Bearer tokens work well for:** * API keys and service tokens * Service-to-service calls * CI/CD pipelines * Backend integrations **Use OAuth instead for:** * User-facing apps * Delegated access (“act on behalf of a user”) * Consent flows and refresh tokens ## Standalone `dedalus_mcp` client If you’re connecting directly to an MCP server that expects a Bearer token, pass `BearerAuth` into `MCPClient.connect(...)`: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp.client import BearerAuth, MCPClient client = await MCPClient.connect( "http://127.0.0.1:8000/mcp", auth=BearerAuth(access_token="your-token"), ) ``` This sends: ```http theme={"theme":{"light":"github-light","dark":"github-dark"}} Authorization: Bearer your-token ``` # Elicitation Source: https://docs.dedaluslabs.ai/sdk/mcp/python/client/elicitation Gather user input during tool execution Servers can request user input during tool execution via elicitation. The client handler collects input matching a JSON schema and returns it to the server. ## Handler ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp.client import ClientCapabilitiesConfig, open_connection from dedalus_mcp.types import ( ElicitRequestParams, ElicitResult, ErrorData, ) async def elicitation_handler( context: object, params: ElicitRequestParams, ) -> ElicitResult | ErrorData: """Handle elicitation requests from the server.""" print(f"\nServer requests input: {params.message}") # Collect values for each field in the schema content = {} schema = params.requestedSchema properties = schema.get("properties", {}) required = schema.get("required", []) for field, field_schema in properties.items(): field_type = field_schema.get("type", "string") is_required = field in required value = input(f"{field} ({field_type}): ") if not value and is_required: return ElicitResult(action="decline") # Type coercion if field_type == "boolean": content[field] = value.lower() in ("true", "yes", "1", "y") elif field_type == "integer": content[field] = int(value) elif field_type == "number": content[field] = float(value) else: content[field] = value return ElicitResult(action="accept", content=content) ``` ## Usage ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} capabilities = ClientCapabilitiesConfig(elicitation=elicitation_handler) async with open_connection( url="http://127.0.0.1:8000/mcp", transport="streamable-http", capabilities=capabilities, ) as client: # Server can now request user input during tool execution result = await client.call_tool("deploy", {"env": "production"}) ``` ## Actions The handler returns one of three actions: | Action | Description | | --------- | ------------------------------------------------- | | `accept` | Submit collected content to the server | | `decline` | Reject the request without canceling the workflow | | `cancel` | Terminate the operation entirely | ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} # Accept with content return ElicitResult(action="accept", content={"confirmed": True}) # Decline (user refused) return ElicitResult(action="decline") # Cancel (abort operation) return ElicitResult(action="cancel") ``` ## Auto-Accept Example For non-interactive environments, auto-accept with defaults: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} async def elicitation_handler(context, params): """Auto-accept with default values for testing.""" content = {} properties = params.requestedSchema.get("properties", {}) for field, schema in properties.items(): field_type = schema.get("type", "string") if field_type == "boolean": content[field] = True elif field_type in ("integer", "number"): content[field] = 0 else: content[field] = "auto-filled" return ElicitResult(action="accept", content=content) ``` # Overview Source: https://docs.dedaluslabs.ai/sdk/mcp/python/client/index Programmatic client for interacting with MCP servers `MCPClient` is an async Python client for talking to any MCP server. It handles the MCP handshake (`initialize`), transport setup, and session management—so you can focus on the operations you want to perform. Use `MCPClient` when you want **explicit, predictable control**, like: * **Testing MCP servers** during development * **Building applications** that need reliable MCP interactions * **Building higher-level clients** (including "agentic" flows) on top of a typed protocol layer ## Quick start Connect → do work → close. ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio from dedalus_mcp.client import MCPClient async def main(): # Local dev default when running `await server.serve()` client = await MCPClient.connect("http://127.0.0.1:8000/mcp") try: tools = await client.list_tools() result = await client.call_tool("add", {"a": 5, "b": 3}) print(result) finally: await client.close() asyncio.run(main()) ``` Prefer `async with` if you want guaranteed cleanup: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp.client import MCPClient async with await MCPClient.connect("http://127.0.0.1:8000/mcp") as client: tools = await client.list_tools() ``` ## Connection ### Connect ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp.client import MCPClient client = await MCPClient.connect("http://127.0.0.1:8000/mcp") ``` ### Protocol info After connecting, `initialize_result` is populated: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} print(f"Server: {client.initialize_result.serverInfo.name}") print(f"Protocol: {client.initialize_result.protocolVersion}") print(f"Session: {client.session_id}") ``` ### Close ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} await client.close() ``` ### Alternative: `open_connection(...)` `open_connection(...)` is a convenience wrapper that yields a connected `MCPClient` and cleans up automatically: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp.client import open_connection async with open_connection("http://127.0.0.1:8000/mcp") as client: tools = await client.list_tools() ``` ### Ping ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} await client.ping() ``` ## Operations Most client code falls into one of these buckets: List and call server-side functions List and read data sources List and get message templates ## Client capabilities (server → client) Some MCP features are initiated by the server. If you want to support them, you configure handlers when connecting. Handle LLM completion requests from servers Gather user input during tool execution Advertise filesystem boundaries to servers Receive log messages from servers ## Authentication For protected MCP servers: API keys and service tokens User consent and delegated access via browser flow ### DPoP authentication For servers using DPoP (RFC 9449) sender-constrained tokens: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp.auth.dpop import generate_dpop_keypair from dedalus_mcp.client import DPoPAuth, MCPClient dpop_key, _ = generate_dpop_keypair() auth = DPoPAuth(access_token="eyJ...", dpop_key=dpop_key) # Local dev: client = await MCPClient.connect("http://127.0.0.1:8000/mcp", auth=auth) # Production: # client = await MCPClient.connect("", auth=auth) ``` # Logging Source: https://docs.dedaluslabs.ai/sdk/mcp/python/client/logging Receive log messages from servers Servers can send log messages to clients during tool execution. This provides visibility into server operations and helps with debugging. ## Handler ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp.client import ClientCapabilitiesConfig, open_connection from dedalus_mcp.types import LoggingMessageNotificationParams def logging_handler(params: LoggingMessageNotificationParams) -> None: """Handle log messages from the server.""" level = params.level.upper() data = params.data print(f"[{level}] {data}") ``` ## Usage ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} capabilities = ClientCapabilitiesConfig(logging=logging_handler) async with open_connection( url="http://127.0.0.1:8000/mcp", transport="streamable-http", capabilities=capabilities, ) as client: # Server logs will be routed to your handler result = await client.call_tool("process", {"file": "data.csv"}) ``` ## Log Levels Servers can send logs at different levels: | Level | Description | | --------- | ------------------------------ | | `debug` | Detailed debugging information | | `info` | General operational messages | | `warning` | Warning conditions | | `error` | Error conditions | ## Structured Logging Route to your logging framework: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} import logging logger = logging.getLogger("mcp.server") def logging_handler(params: LoggingMessageNotificationParams) -> None: level = getattr(logging, params.level.upper(), logging.INFO) logger.log(level, params.data) ``` ## Full Example ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio from dedalus_mcp.client import ClientCapabilitiesConfig, open_connection from dedalus_mcp.types import LoggingMessageNotificationParams def logging_handler(params: LoggingMessageNotificationParams) -> None: level = params.level.upper() print(f"[SERVER {level}] {params.data}") async def main(): capabilities = ClientCapabilitiesConfig(logging=logging_handler) async with open_connection( url="http://127.0.0.1:8000/mcp", transport="streamable-http", capabilities=capabilities, ) as client: print(f"Connected: {client.initialize_result.serverInfo.name}") tools = await client.list_tools() print(f"Available tools: {[t.name for t in tools.tools]}") # Call a tool - logs will appear via handler result = await client.call_tool("analyze", {"input": "test"}) print(f"Result: {result.content}") asyncio.run(main()) ``` # OAuth Source: https://docs.dedaluslabs.ai/sdk/mcp/python/client/oauth Authenticate with OAuth browser flow Use OAuth for MCP servers that require user consent, like Gmail, Google Calendar, or other services with delegated access. ## OAuth Flow The OAuth flow is triggered when you call an MCP server that requires user authentication. The SDK calls the MCP server. If no valid token exists, the server returns `401` with a `WWW-Authenticate` header. The SDK fetches `/.well-known/oauth-protected-resource` (RFC 9728) to discover the authorization server and supported scopes. The SDK raises `AuthenticationError` containing a `connect_url`—the full OAuth authorization URL. Your app opens the user's browser to the `connect_url`. The user logs in and grants (or denies) the requested scopes. Upon approval, the authorization server exchanges the authorization code for tokens using PKCE. DAuth stores the tokens server-side. The user returns to your app and triggers a retry. The SDK re-sends the request, now with valid credentials. The access token is automatically included for subsequent requests to the MCP server. If the access token expires, DAuth automatically uses the refresh token to obtain a new access token. ## How It Works ```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}} sequenceDiagram participant U as User participant S as SDK participant M as MCP Server participant A as Auth Server U->>S: Request S->>M: Call tool M-->>S: 401 Unauthorized S->>M: GET /.well-known/oauth-protected-resource M-->>S: {authorization_servers, scopes} S-->>U: AuthenticationError (connect_url) U->>A: Open browser, login + consent A-->>U: Redirect (tokens stored) U->>S: Retry S->>M: Call tool + token M-->>S: Response ``` ## OAuth Retry Helper Handle the OAuth flow with a retry wrapper: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} import webbrowser from collections.abc import Awaitable, Callable from typing import TypeVar from dedalus_labs import AuthenticationError T = TypeVar("T") async def with_oauth_retry(fn: Callable[[], Awaitable[T]]) -> T: """Run async function, handling OAuth browser flow if needed.""" try: return await fn() except AuthenticationError as e: body = e.body if isinstance(e.body, dict) else {} url = body.get("connect_url") or body.get("detail", {}).get("connect_url") if not url: raise print("\nOpening browser for OAuth...") print("If the browser does not open, visit:\n") print(url) webbrowser.open(url) input("\nPress Enter after completing OAuth...") return await fn() ``` ## Full Example: DedalusRunner ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio import webbrowser from collections.abc import Awaitable, Callable from typing import TypeVar from dotenv import load_dotenv load_dotenv() from dedalus_labs import AsyncDedalus, AuthenticationError, DedalusRunner T = TypeVar("T") async def with_oauth_retry(fn: Callable[[], Awaitable[T]]) -> T: try: return await fn() except AuthenticationError as e: body = e.body if isinstance(e.body, dict) else {} url = body.get("connect_url") or body.get("detail", {}).get("connect_url") if not url: raise webbrowser.open(url) input("\nPress Enter after completing OAuth...") return await fn() async def main(): client = AsyncDedalus() runner = DedalusRunner(client) result = await with_oauth_retry( lambda: runner.run( input="List my recent emails and summarize them", model="openai/gpt-4.1", mcp_servers=["anny_personal/gmail-mcp"], ) ) print(result.output) if result.mcp_results: for r in result.mcp_results: print(f"{r.tool_name} ({r.duration_ms}ms): {r.result}") asyncio.run(main()) ``` ## Full Example: Raw Client For single requests with full control over API response: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio import webbrowser from collections.abc import Awaitable, Callable from typing import TypeVar from dotenv import load_dotenv load_dotenv() from dedalus_labs import AsyncDedalus, AuthenticationError T = TypeVar("T") async def with_oauth_retry(fn: Callable[[], Awaitable[T]]) -> T: try: return await fn() except AuthenticationError as e: body = e.body if isinstance(e.body, dict) else {} url = body.get("connect_url") or body.get("detail", {}).get("connect_url") if not url: raise webbrowser.open(url) input("\nPress Enter after completing OAuth...") return await fn() async def main(): client = AsyncDedalus() async def do_request(): return await client.chat.completions.create( model="openai/gpt-4.1", messages=[ { "role": "user", "content": "List my recent emails and summarize them", } ], mcp_servers=["anny_personal/gmail-mcp"], ) resp = await with_oauth_retry(do_request) print(resp.choices[0].message.content) if resp.mcp_tool_results: for r in resp.mcp_tool_results: print(f"{r.tool_name} ({r.duration_ms}ms): {r.result}") asyncio.run(main()) ``` ## Environment ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} # .env DEDALUS_API_KEY=dsk-... DEDALUS_API_URL=https://api.dedaluslabs.ai DEDALUS_AS_URL=https://as.dedaluslabs.ai ``` No OAuth credentials needed client-side. The MCP server handles OAuth configuration, and DAuth manages token storage. ## When to Use **OAuth works for:** * User-facing applications * Delegated access (acting on behalf of users) * Services like Gmail, Google Calendar, Linear, GitHub **Use [Bearer Auth](/sdk/mcp/python/client/bearer-auth) instead for:** * API keys and service tokens * Backend integrations without user context * Service-to-service calls # Prompts Source: https://docs.dedaluslabs.ai/sdk/mcp/python/client/prompts List and render prompts from MCP servers # Prompts > List and render prompts from MCP servers Prompts are reusable message templates exposed by the server. A prompt can accept **string-valued arguments** and returns a sequence of messages you can feed into an LLM conversation. **If you’re new to this:** think of a prompt as a **pre-written chat script** stored on the server (like “summarize” or “code review”). Your client can **list** the available scripts, then **render** one into actual messages (optionally filling in arguments like `"language": "python"`). ## List prompts Discover available prompts: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp.client import open_connection async with open_connection("http://127.0.0.1:8000/mcp") as client: prompts = await client.list_prompts() for p in prompts.prompts: print(f"{p.name}: {p.description}") ``` ## Prompt schema Each prompt includes: | Field | Type | Description | | ------------- | -------------- | --------------------------- | | `name` | `str` | Prompt identifier | | `description` | `str \| None` | What the prompt does | | `arguments` | `list \| None` | Required/optional arguments | ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} for p in prompts.prompts: print(f"Name: {p.name}") print(f"Description: {p.description}") if p.arguments: for arg in p.arguments: required = "(required)" if arg.required else "(optional)" print(f" - {arg.name}: {arg.description} {required}") ``` ## Get a prompt Render a prompt with arguments: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} result = await client.get_prompt("summarize", {"style": "brief"}) print(result.messages) ``` **Note**: MCP prompt arguments are strings. If a server expects structured input (lists/dicts), it will usually ask you to pass a JSON string and parse it server-side. ## Response structure A rendered prompt returns a `GetPromptResult`: | Field | Type | Description | | ------------- | ------------- | ------------------------- | | `messages` | `list` | Rendered message sequence | | `description` | `str \| None` | Optional description | Each message has a `role` and a `content` block. Many prompts return text content; some may return non-text content blocks. ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import types result = await client.get_prompt("code-review", {"language": "python"}) for msg in result.messages: content = msg.content if isinstance(content, types.TextContent): text = content.text else: text = f"<{content.type} content>" print(f"[{msg.role}] {text}") ``` ## Example: Code assistant ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio from dedalus_mcp.client import MCPClient from dedalus_mcp import types async def main(): client = await MCPClient.connect("http://127.0.0.1:8000/mcp") try: prompts = await client.list_prompts() print("Available prompts:") for p in prompts.prompts: print(f" - {p.name}: {p.description}") result = await client.get_prompt( "explain-code", { "language": "python", "code": "def fib(n): return n if n < 2 else fib(n-1) + fib(n-2)", }, ) print("\nRendered prompt:") for msg in result.messages: if isinstance(msg.content, types.TextContent): print(f"[{msg.role}] {msg.content.text}") else: print(f"[{msg.role}] <{msg.content.type} content>") finally: await client.close() asyncio.run(main()) ``` ## Prompts without arguments Some prompts don’t require arguments: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} result = await client.get_prompt("greeting") # or explicitly: result = await client.get_prompt("greeting", {}) ``` ## Context manager ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp.client import open_connection async with open_connection("http://127.0.0.1:8000/mcp") as client: prompts = await client.list_prompts() result = await client.get_prompt("analyze", {"data": "sample"}) ``` # Resources Source: https://docs.dedaluslabs.ai/sdk/mcp/python/client/resources List and read resources from MCP servers Resources are **read-only data** exposed by the server. A resource might be a file, a generated report, or any other content the server can provide without side effects. ## List resources ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp.client import open_connection async with open_connection("http://127.0.0.1:8000/mcp") as client: resources = await client.list_resources() for r in resources.resources: print(f"{r.uri}: {r.name}") ``` ## Resource schema Each resource includes: | Field | Type | Description | | ------------- | ------------- | -------------------------------------------------- | | `uri` | `str` | Resource identifier (e.g. `resource://config/app`) | | `name` | `str` | Human-readable name | | `description` | `str \| None` | What the resource contains | | `mimeType` | `str \| None` | Content type (e.g. `application/json`) | ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} for r in resources.resources: print(f"URI: {r.uri}") print(f"Name: {r.name}") print(f"Type: {r.mimeType}") ``` ## Read resources Read a specific resource by URI: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} result = await client.read_resource("resource://config/app") ``` `result` is a `ReadResourceResult` with a `contents` list. It can contain `TextResourceContents` (text) or `BlobResourceContents` (base64 blob). ## Response structure (text vs binary) ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} import base64 from dedalus_mcp import types result = await client.read_resource("resource://data/report") if not result.contents: print("No contents (resource missing or empty)") else: item = result.contents[0] if isinstance(item, types.TextResourceContents): print(item.text) elif isinstance(item, types.BlobResourceContents): data = base64.b64decode(item.blob) # blob is base64 text with open("output.bin", "wb") as f: f.write(data) ``` ## Example: Configuration reader ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} import json from dedalus_mcp.client import open_connection from dedalus_mcp import types async with open_connection("http://127.0.0.1:8000/mcp") as client: resources = await client.list_resources() print("Available resources:") for r in resources.resources: print(f" - {r.uri} ({r.mimeType})") result = await client.read_resource("resource://config/app") if not result.contents or not isinstance(result.contents[0], types.TextResourceContents): raise RuntimeError("Expected text config resource") data = json.loads(result.contents[0].text) print(f"App config: {data}") ``` ## Resource templates Resource templates are just "patterns" a server can publish (like `resource://users/{user_id}`) to show what kinds of resource URLs exist. They don't automatically create resources. To read something, you still call `read_resource(...)` with a real URL (like `resource://users/123`), and it only works if the server actually serves that exact URI. ## Context manager Again, `open_connection(...)` is an async context manager. It means you don't have to remember to call await `client.close()`. When the async with block exits, it automatically closes the underlying connection for you. ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp.client import open_connection async with open_connection("http://127.0.0.1:8000/mcp") as client: resources = await client.list_resources() result = await client.read_resource("resource://docs/readme") ``` # Roots Source: https://docs.dedaluslabs.ai/sdk/mcp/python/client/roots Advertise filesystem boundaries to servers Roots inform servers about filesystem resources the client has access to. Servers can use this information to adjust behavior or restrict operations to safe boundaries. ## Configure Roots ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from pathlib import Path from dedalus_mcp.client import ClientCapabilitiesConfig, open_connection from dedalus_mcp.types import Root initial_roots = [ Root(uri=Path.cwd().as_uri(), name="Project Directory"), Root(uri=Path("/tmp").as_uri(), name="Temporary Files"), ] capabilities = ClientCapabilitiesConfig( enable_roots=True, initial_roots=initial_roots, ) async with open_connection( url="http://127.0.0.1:8000/mcp", transport="streamable-http", capabilities=capabilities, ) as client: # List advertised roots roots = await client.list_roots() for root in roots: print(f"{root.name}: {root.uri}") ``` ## Dynamic Updates Update roots during the session: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} async with open_connection( url="http://127.0.0.1:8000/mcp", transport="streamable-http", capabilities=capabilities, ) as client: # Add a new root new_roots = initial_roots + [ Root(uri=Path.home().as_uri(), name="Home Directory"), ] await client.update_roots(new_roots, notify=True) # Verify update roots = await client.list_roots() print(f"Now advertising {len(roots)} roots") ``` ## Root Structure ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp.types import Root Root( uri="file:///path/to/directory", # File URI name="Human-readable name", # Optional display name ) ``` | Field | Type | Description | | ------ | ----- | -------------------------------------------- | | `uri` | `str` | File URI (e.g., `file:///home/user/project`) | | `name` | `str` | Optional human-readable name | ## Security Roots establish security boundaries. Servers should: * Only access files within advertised roots * Reject operations targeting paths outside root boundaries * Use roots to scope file searches and operations ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} # Server-side: check if path is within roots def is_path_allowed(path: Path, roots: list[Root]) -> bool: for root in roots: root_path = Path(str(root.uri).replace("file://", "")) if path.is_relative_to(root_path): return True return False ``` # Sampling Source: https://docs.dedaluslabs.ai/sdk/mcp/python/client/sampling Handle LLM completion requests from servers # Sampling > Handle LLM completion requests from servers MCP servers can request LLM completions from clients during tool execution. This enables servers to delegate AI reasoning to the client, which controls which model is used. ## Handler To support sampling, register a sampling handler when you connect. The handler receives the server’s request (`CreateMessageRequestParams`) and should return either: * `CreateMessageResult` (success), or * `ErrorData` (failure) Here’s a complete example using **Anthropic**: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from anthropic import AsyncAnthropic from dedalus_mcp import types anthropic = AsyncAnthropic() async def sampling_handler( _ctx: object, params: types.CreateMessageRequestParams, ) -> types.CreateMessageResult | types.ErrorData: try: messages = [{"role": m.role, "content": m.content.text} for m in params.messages] resp = await anthropic.messages.create( model="claude-sonnet-4-20250514", messages=messages, max_tokens=params.maxTokens, ) return types.CreateMessageResult( model=resp.model, role="assistant", content=types.TextContent(type="text", text=resp.content[0].text), stopReason="end_turn", ) except Exception as e: return types.ErrorData(code=types.INTERNAL_ERROR, message=str(e)) ``` ## Usage Enable sampling by passing the handler in `ClientCapabilitiesConfig` when connecting: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} capabilities = ClientCapabilitiesConfig(sampling=sampling_handler) async with open_connection( url="http://127.0.0.1:8000/mcp", capabilities=capabilities, ) as client: # If the server calls sampling/createMessage during this tool run, # your sampling_handler will be invoked. result = await client.call_tool("analyze", {"data": "..."}) ``` ## Error handling When something goes wrong inside your handler, return an `ErrorData` (don’t raise). The server will receive this as an MCP error response to its sampling request. ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import types async def sampling_handler(context: object, params: types.CreateMessageRequestParams): try: # ... call LLM ... return types.CreateMessageResult( model="claude-3-5-sonnet-20241022", role="assistant", content=types.TextContent(type="text", text="ok"), stopReason="end_turn", ) except Exception as e: return types.ErrorData(code=types.INTERNAL_ERROR, message=str(e)) ``` # Tools Source: https://docs.dedaluslabs.ai/sdk/mcp/python/client/tools List and call tools on MCP servers Tools are server-side functions that a client can execute with arguments. With `MCPClient`, the basic flow is: ## List tools Discover available tools on the server: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp.client import open_connection async with open_connection("http://127.0.0.1:8000/mcp") as client: tools = await client.list_tools() for tool in tools.tools: print(f"{tool.name}: {tool.description}") ``` ## Tool schema Each tool includes: | Field | Type | Description | | ------------- | ------ | ------------------------- | | `name` | `str` | Tool identifier | | `description` | `str` | What the tool does | | `inputSchema` | `dict` | JSON Schema for arguments | ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} for tool in tools.tools: print(f"Name: {tool.name}") print(f"Description: {tool.description}") print(f"Parameters schema: {tool.inputSchema}") ``` ## Call tools Execute a tool with arguments: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} result = await client.call_tool("add", {"a": 5, "b": 3}) # Tool results are content blocks; most tools return TextContent. print(result.content[0].text) ``` ## Error handling If the server returns a JSON-RPC error (common when a tool raises), `call_tool(...)` raises `McpError`: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from mcp.shared.exceptions import McpError try: result = await client.call_tool("divide", {"a": 10, "b": 0}) print(result.content[0].text) except McpError as e: print(f"Tool call failed: {e}") ``` ## Example: Calculator ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} import asyncio from dedalus_mcp.client import MCPClient from mcp.shared.exceptions import McpError async def main(): client = await MCPClient.connect("http://127.0.0.1:8000/mcp") try: tools = await client.list_tools() print("Available tools:") for tool in tools.tools: print(f" - {tool.name}") add_result = await client.call_tool("add", {"a": 5, "b": 3}) print(f"5 + 3 = {add_result.content[0].text}") mul_result = await client.call_tool("multiply", {"a": 4, "b": 7}) print(f"4 * 7 = {mul_result.content[0].text}") except McpError as e: print(f"Tool call failed: {e}") finally: await client.close() asyncio.run(main()) ``` ## Context manager `open_connection(...)` is an async context manager. It means you don't have to remember to call await `client.close()`. When the async with block exits, it automatically closes the underlying connection for you. ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp.client import open_connection async with open_connection("http://127.0.0.1:8000/mcp") as client: tools = await client.list_tools() result = await client.call_tool("greet", {"name": "World"}) print(result.content[0].text) ``` # Connections Source: https://docs.dedaluslabs.ai/sdk/mcp/python/connections How connections are named and resolved ## Single-Connection Servers For single-connection servers, name is optional and dispatch auto-resolves: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} gmail = Connection(secrets=SecretKeys(token="GMAIL_TOKEN")) # No name needed async def _req(method, path, body=None): ctx = get_context() return await ctx.dispatch(HttpRequest(method=method, path=path, body=body)) ``` ## Connection Naming Connection names are derived from your server's slug: `windsor/gmail-mcp` -> `gmail-mcp`. If you hardcode a different name in `dispatch("gmail", ...)`, it will fail. Use auto-dispatch for single-connection servers. ## Multi-Connection Servers When you have multiple connections, specify the target explicitly: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} await ctx.dispatch("gmail", HttpRequest(...)) await ctx.dispatch("calendar", HttpRequest(...)) ``` Each connection requires a separate OAuth flow. Pass `connection_name` when creating the session: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} # API creates session with explicit connection name session = await admin_api.create_oauth_session( server_id=deployment_id, scopes=["gmail.readonly"], connection_name="gmail", # Stored with this name ) ``` ## Debugging Connection not found? Check what's in the JWT: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} @tool(description="Debug") async def debug() -> dict: ctx = get_context() return {"connections": list(ctx.runtime.get("connections", {}).keys())} ``` # DAuth Architecture Source: https://docs.dedaluslabs.ai/sdk/mcp/python/dauth-architecture How DAuth protects credentials with sealed execution DAuth provides credential isolation through a sealed execution model. Your MCP server never sees raw credentials—only opaque handles that reference secrets stored and decrypted within a secure boundary. ## The Flow ```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}} sequenceDiagram participant U as SDK participant P as Dedalus participant CP as DAuth participant M as MCP Server participant S as Sealed Enclave Note over U: 1) Encrypt credentials client-side U->>U: Encrypt credentials Note over U,P: 2) Send request with encrypted credentials U->>P: Request + encrypted credentials Note over P,CP: 3) Send scoped request P->>CP: Scoped request CP-->>P: Scoped token Note over P,M: 4) Call MCP server with scoped token P->>M: Tool call + scoped token M->>M: Validate token Note over M,S: 5) Execute in sealed boundary M->>S: Execute request S->>S: External API S-->>M: Result M-->>P: Tool result P-->>U: Response ``` ## Step-by-Step ### 1. Client-Side Encryption Credentials are encrypted on your device before transmission. Plaintext secrets never travel over the network. ### 2. Request with Encrypted Credentials The SDK sends your request along with encrypted credentials to Dedalus. ### 3. Scoped Token Issuance DAuth stores the encrypted credentials and issues a scoped token that: * Is bound to specific MCP servers * Is cryptographically bound to your client's key (DPoP) * Can only be used for authorized operations ### 4. MCP Server Receives Token Your MCP server receives the scoped token and validates it against DAuth's public keys. The server never sees raw credentials. ### 5. Sealed Execution When the MCP server needs to call an external API (GitHub, Slack, etc.), it dispatches to a **sealed enclave**: * Decrypts credentials using hardware-backed keys * Calls the external API via TLS connection * Returns only the response * Scrubs credentials from memory immediately The response flows back through the MCP server to your application. At no point did your code have access to raw secrets. ## Security Properties | Property | What It Means | | ----------------------------- | ------------------------------------------------------ | | **Client-side encryption** | Credentials encrypted before leaving your device | | **Scoped tokens** | Tokens are limited to specific servers and connections | | **Sealed execution** | Decryption happens in isolated hardware boundary | | **Sender-constrained (DPoP)** | Stolen tokens are useless without the private key | | **No credential persistence** | Secrets decrypted on-demand, scrubbed after use | ## Why This Matters Traditional architectures require your application to handle credentials directly: ``` User → App → [credentials in memory] → External API → User ``` With DAuth: ``` User → Encrypted Token → Scoped Request → Sealed Boundary (App or External API) → User ``` Your application code, logs, and error traces never contain raw secrets. Even if your MCP server is compromised, attackers cannot extract credentials—they exist only within the sealed execution boundary. ## Using DAuth See [Authorization](/sdk/mcp/python/authorization) for implementation details: * Enable DAuth with `AuthorizationConfig` * Configure server-level and per-tool scopes * Access authenticated user claims in your tools # Debugging Source: https://docs.dedaluslabs.ai/sdk/mcp/python/debugging Debug MCP servers MCP involves two processes talking over HTTP or stdio. Here's how to debug them. ## The tmux pattern Run server and client in split terminals: ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} tmux new-session -d -s mcp tmux split-window -h tmux send-keys -t mcp:0.0 'python server.py' C-m sleep 2 tmux send-keys -t mcp:0.1 'python client.py' C-m tmux attach -t mcp ``` See both outputs side by side. Kill when done: `tmux kill-session -t mcp` ## Structured logging Log from inside tools: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import get_context, tool @tool(description="Process data") async def process(data: str) -> dict: ctx = get_context() await ctx.debug("Starting", data={"length": len(data)}) # ... work ... await ctx.info("Done", data={"result": 42}) return {"ok": True} ``` ## Verbose mode ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} await server.serve(verbose=True, log_level="debug") ``` Or via environment: ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} LOG_LEVEL=debug python server.py ``` ## Client-side log capture ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp.client import MCPClient, ClientCapabilitiesConfig def log_handler(params): print(f"[{params.level}] {params.data}") config = ClientCapabilitiesConfig(logging=log_handler) client = await MCPClient.connect(url, capabilities=config) ``` ## Common issues **"Client does not advertise the sampling capability"** Pass a sampling handler: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} config = ClientCapabilitiesConfig(sampling=sampling_handler) client = await MCPClient.connect(url, capabilities=config) ``` **"Tool mutation attempted after server startup"** Enable dynamic tools: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} server = MCPServer("my-server", allow_dynamic_tools=True) ``` **Connection refused** Check the port: `lsof -i :8000` Kill orphans: `lsof -ti :8000 | xargs kill -9` **Wrong URL** The endpoint is `http://127.0.0.1:8000/mcp`, not just `:8000`. ## When stuck 1. Strip to minimal reproduction 2. Check MCP spec compliance 3. Test with MCP Inspector: `pnpm dlx @anthropic/mcp-inspector` 4. Read server logs # Deploy Source: https://docs.dedaluslabs.ai/sdk/mcp/python/deploy Host your MCP server and share it with others. Deploy your server to the Dedalus platform. Once deployed, you can: * **Access it from anywhere** — No local server required. * **Share with others** — Let anyone use your MCP server. * **Monetize** — In the future, earn revenue sharing when others use your server. Go to [dedaluslabs.ai](https://www.dedaluslabs.ai) and click **Dashboard**. Dedalus homepage with Dashboard button Click `Add Server` to create a new deployment. Servers page with Add Server button Select your GitHub repository. Dedalus pulls from your repo on each deploy. Connect GitHub repository modal Configure your server: * **Environment Variables**: Your API keys (e.g., `OPENAI_API_KEY`). Encrypted and only accessible to your server. * **Required Credentials**: Fields users must provide (e.g., Supabase key, X API key). Users supply their own credentials at runtime. Click `Deploy` when ready. Deploy server configuration Once deployed, click `Publish` to list your server on the Dedalus MCP marketplace. In the future, when others use your server, you earn revenue sharing on every API call. Publish server to marketplace Your server is now live. Use your slug in the Dedalus SDK: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} mcp_servers=["your-org/your-server"] ``` Pro users get a server URL to add your server to Cursor, Claude, or any MCP client in one click. ## Tips Your repository should follow this structure: ``` my-server/ ├── main.py # Required: server entry point ├── pyproject.toml # Required: dependencies ├── tools/ # Optional: organize tools in a folder │ ├── __init__.py │ ├── search.py │ └── fetch.py └── ... ``` You can define tools directly in `main.py` or split them into a `tools/` folder for larger servers. For OAuth servers, the `name` parameter in `Connection("my-server", ...)` must match your deployment slug exactly (not including the org prefix). Same applies to `ctx.dispatch("my-server")`. This ensures OAuth callbacks route correctly. **Environment Variables** are your secrets (e.g., `OPENAI_API_KEY`). They're encrypted and only accessible to your server. **Required Credentials** are fields users must provide when connecting to your server (e.g., their own API keys). Users supply these at runtime. If your build fails, check the build logs in your dashboard. Common issues: * Missing dependencies in `pyproject.toml` * Environment variables not set # Examples Source: https://docs.dedaluslabs.ai/sdk/mcp/python/examples Production MCP server examples Real-world MCP servers you can deploy today. Each example shows credential setup, tool implementation, and server configuration. ## GitHub + Supabase A multi-connection MCP server exposing GitHub and Supabase tools. ### Environment ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} # .env DEDALUS_API_KEY=dsk-... # Supabase SUPABASE_URL=https://xxx.supabase.co SUPABASE_SECRET_KEY=eyJ... # GitHub GITHUB_TOKEN=ghp_... GITHUB_BASE_URL=https://api.github.com ``` ### Server ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} # server.py import os from dedalus_mcp import MCPServer from dedalus_mcp.server import TransportSecuritySettings from dedalus_mcp.auth import Connection, SecretKeys # Define connections (credentials provided by client at runtime) github = Connection( name="github", secrets=SecretKeys(token="GITHUB_TOKEN"), auth_header_format="token {api_key}" ) supabase = Connection( name="supabase", secrets=SecretKeys(token="SUPABASE_SECRET_KEY"), auth_header_format="Bearer {api_key}" ) def create_server() -> MCPServer: return MCPServer( name="example-dedalus-mcp", connections=[github, supabase], http_security=TransportSecuritySettings(enable_dns_rebinding_protection=False), streamable_http_stateless=True, authorization_server=os.getenv("DEDALUS_AS_URL", "https://as.dedaluslabs.ai"), ) ``` ### GitHub tools ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} # gh.py from dataclasses import dataclass from typing import Any from dedalus_mcp import HttpMethod, HttpRequest, get_context, tool from dedalus_mcp.types import ToolAnnotations @dataclass(frozen=True) class GhResult: """GitHub API result.""" success: bool data: Any = None error: str | None = None @dataclass(frozen=True) class GhUser: """GitHub user profile.""" login: str name: str | None bio: str | None public_repos: int followers: int @dataclass(frozen=True) class GhRepo: """GitHub repository summary.""" name: str full_name: str stars: int language: str | None updated_at: str async def _gh_request(method: HttpMethod, path: str, body: Any = None) -> GhResult: """Execute GitHub API request.""" ctx = get_context() resp = await ctx.dispatch("github", HttpRequest(method=method, path=path, body=body)) if resp.success: return GhResult(success=True, data=resp.response.body) return GhResult(success=False, error=resp.error.message if resp.error else "Request failed") @tool( description="Get the authenticated GitHub user's profile", tags=["user", "read"], annotations=ToolAnnotations(readOnlyHint=True), ) async def gh_whoami() -> GhResult: result = await _gh_request(HttpMethod.GET, "/user") if result.success and result.data: user = GhUser( login=result.data["login"], name=result.data.get("name"), bio=result.data.get("bio"), public_repos=result.data.get("public_repos", 0), followers=result.data.get("followers", 0), ) return GhResult(success=True, data=user) return result @tool( description="List repositories for the authenticated user", tags=["repos", "read"], annotations=ToolAnnotations(readOnlyHint=True), ) async def gh_list_repos(per_page: int = 10) -> GhResult: result = await _gh_request(HttpMethod.GET, f"/user/repos?per_page={per_page}&sort=updated") if result.success and isinstance(result.data, list): repos = [ GhRepo( name=r["name"], full_name=r["full_name"], stars=r.get("stargazers_count", 0), language=r.get("language"), updated_at=r.get("updated_at", ""), ) for r in result.data ] return GhResult(success=True, data=repos) return result ``` *** ## X (Twitter) API Read-only X API tools using OAuth 2.0 App-Only authentication. ### Environment ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} # .env X_BEARER_TOKEN=AAAA... # App-only bearer token X_API_KEY=... # Optional: for user-context endpoints X_API_KEY_SECRET=... # Optional: for user-context endpoints ``` Free tier limits: 100 tweet reads, 500 writes/month. For production, consider Basic (\$100/mo) or Pro tier. ### Connection ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} # x.py from dataclasses import dataclass from typing import Any from dedalus_mcp import HttpMethod, HttpRequest, get_context, tool from dedalus_mcp.auth import Connection, SecretKeys from dedalus_mcp.types import ToolAnnotations x = Connection( name="x", secrets=SecretKeys(token="X_BEARER_TOKEN"), auth_header_format="Bearer {api_key}", ) DEFAULT_TWEET_FIELDS = "id,text,author_id,created_at,public_metrics" DEFAULT_USER_FIELDS = "id,name,username,description,public_metrics" @dataclass(frozen=True) class XResult: """X API result.""" success: bool data: Any = None meta: dict | None = None error: str | None = None @dataclass(frozen=True) class XUser: """X user profile.""" id: str name: str username: str description: str | None followers_count: int following_count: int @dataclass(frozen=True) class XTweet: """X tweet.""" id: str text: str author_id: str created_at: str retweet_count: int like_count: int async def _x_request(path: str) -> XResult: """Execute X API request.""" ctx = get_context() resp = await ctx.dispatch("x", HttpRequest(method=HttpMethod.GET, path=path)) if resp.success: body = resp.response.body or {} return XResult(success=True, data=body.get("data"), meta=body.get("meta")) return XResult(success=False, error=resp.error.message if resp.error else "Request failed") ``` ### Tools ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} @tool( description="Get an X user by their username", tags=["user", "read"], annotations=ToolAnnotations(readOnlyHint=True), ) async def x_get_user_by_username(username: str) -> XResult: result = await _x_request(f"/2/users/by/username/{username}?user.fields={DEFAULT_USER_FIELDS}") if result.success and result.data: metrics = result.data.get("public_metrics", {}) user = XUser( id=result.data["id"], name=result.data["name"], username=result.data["username"], description=result.data.get("description"), followers_count=metrics.get("followers_count", 0), following_count=metrics.get("following_count", 0), ) return XResult(success=True, data=user) return result @tool( description="Search recent tweets (last 7 days)", tags=["search", "read"], annotations=ToolAnnotations(readOnlyHint=True), ) async def x_search_recent(query: str, max_results: int = 10) -> XResult: from urllib.parse import quote max_results = max(10, min(100, max_results)) result = await _x_request( f"/2/tweets/search/recent?query={quote(query)}&tweet.fields={DEFAULT_TWEET_FIELDS}&max_results={max_results}" ) if result.success and result.data: tweets = [ XTweet( id=t["id"], text=t["text"], author_id=t["author_id"], created_at=t.get("created_at", ""), retweet_count=t.get("public_metrics", {}).get("retweet_count", 0), like_count=t.get("public_metrics", {}).get("like_count", 0), ) for t in result.data ] return XResult(success=True, data=tweets, meta=result.meta) return result @tool( description="Get a user's recent tweets", tags=["tweet", "read"], annotations=ToolAnnotations(readOnlyHint=True), ) async def x_get_user_tweets(user_id: str, max_results: int = 10) -> XResult: max_results = max(5, min(100, max_results)) result = await _x_request( f"/2/users/{user_id}/tweets?tweet.fields={DEFAULT_TWEET_FIELDS}&max_results={max_results}" ) if result.success and result.data: tweets = [ XTweet( id=t["id"], text=t["text"], author_id=t["author_id"], created_at=t.get("created_at", ""), retweet_count=t.get("public_metrics", {}).get("retweet_count", 0), like_count=t.get("public_metrics", {}).get("like_count", 0), ) for t in result.data ] return XResult(success=True, data=tweets, meta=result.meta) return result ``` *** ## Gmail (OAuth 2.0) Gmail MCP server with true OAuth 2.0 user authentication. ### Environment ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} # .env OAUTH_ENABLED=true OAUTH_AUTHORIZE_URL=https://accounts.google.com/o/oauth2/auth OAUTH_TOKEN_URL=https://oauth2.googleapis.com/token OAUTH_CLIENT_ID=your-client-id.apps.googleusercontent.com OAUTH_CLIENT_SECRET=your-client-secret OAUTH_SCOPES_AVAILABLE=https://www.googleapis.com/auth/gmail.readonly,https://www.googleapis.com/auth/gmail.modify OAUTH_BASE_URL=https://gmail.googleapis.com DEDALUS_API_KEY=dsk-... DEDALUS_API_URL=https://api.dedaluslabs.ai DEDALUS_AS_URL=https://as.dedaluslabs.ai ``` ### Setup 1. **Enable Gmail API**: Go to [Gmail API](https://console.cloud.google.com/apis/library/gmail.googleapis.com) and click "Enable" 2. **Create OAuth credentials**: Go to [APIs & Services → Credentials](https://console.cloud.google.com/apis/credentials) and: * Click "Create Credentials" → "OAuth client ID" * Application type: "Web application" * Add authorized redirect URIs for your deployment * Copy the Client ID and Client Secret to your `.env` 3. **Configure consent screen**: Set up the OAuth consent screen with the Gmail scopes listed above ### Server ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} # server.py import os from dedalus_mcp import MCPServer from dedalus_mcp.server import TransportSecuritySettings from gmail import gmail, gmail_tools def create_server() -> MCPServer: return MCPServer( name="gmail-mcp", connections=[gmail], http_security=TransportSecuritySettings(enable_dns_rebinding_protection=False), streamable_http_stateless=True, authorization_server=os.getenv("DEDALUS_AS_URL", "https://as.dedaluslabs.ai"), ) async def main() -> None: server = create_server() server.collect(*gmail_tools) await server.serve(port=8080) ``` ### Connection ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} # gmail.py import os from dataclasses import dataclass from typing import Any from dedalus_mcp import HttpMethod, HttpRequest, get_context, tool from dedalus_mcp.auth import Connection, OAuthConfig from dedalus_mcp.types import ToolAnnotations gmail = Connection( name="gmail", oauth=OAuthConfig( client_id=os.getenv("OAUTH_CLIENT_ID"), client_secret=os.getenv("OAUTH_CLIENT_SECRET"), authorize_url=os.getenv("OAUTH_AUTHORIZE_URL"), token_url=os.getenv("OAUTH_TOKEN_URL"), scopes=os.getenv("OAUTH_SCOPES_AVAILABLE", "").split(","), ), auth_header_format="Bearer {access_token}", ) @dataclass(frozen=True) class GmailResult: """Gmail API result.""" success: bool data: Any = None error: str | None = None @dataclass(frozen=True) class GmailMessage: """Gmail message summary.""" id: str thread_id: str snippet: str | None label_ids: list[str] async def _gmail_request(method: HttpMethod, path: str) -> GmailResult: """Execute Gmail API request.""" ctx = get_context() resp = await ctx.dispatch("gmail", HttpRequest(method=method, path=path)) if resp.success: return GmailResult(success=True, data=resp.response.body) return GmailResult(success=False, error=resp.error.message if resp.error else "Request failed") ``` ### Tools ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} @tool( description="List recent emails", tags=["email", "read"], annotations=ToolAnnotations(readOnlyHint=True), ) async def gmail_list_messages(max_results: int = 10, query: str = "") -> GmailResult: path = f"/gmail/v1/users/me/messages?maxResults={max_results}" if query: path += f"&q={query}" result = await _gmail_request(HttpMethod.GET, path) if result.success and result.data: messages = [ GmailMessage( id=m["id"], thread_id=m.get("threadId", ""), snippet=None, label_ids=[], ) for m in result.data.get("messages", []) ] return GmailResult(success=True, data=messages) return result @tool( description="Get email details by ID", tags=["email", "read"], annotations=ToolAnnotations(readOnlyHint=True), ) async def gmail_get_message(message_id: str) -> GmailResult: result = await _gmail_request(HttpMethod.GET, f"/gmail/v1/users/me/messages/{message_id}") if result.success and result.data: msg = result.data message = GmailMessage( id=msg["id"], thread_id=msg.get("threadId", ""), snippet=msg.get("snippet"), label_ids=msg.get("labelIds", []), ) return GmailResult(success=True, data=message) return result ``` *** ## Running the examples All examples follow the same pattern: ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} # Clone the repo git clone https://github.com/dedalus-labs/example-dedalus-mcp cd example-dedalus-mcp # For the main example (GitHub + Supabase) cp .env.example .env # Edit .env with your credentials uv sync uv run python src/main.py # For X cd x-mcp cp .env.example .env uv sync uv run python src/main.py # For Gmail cd gmail-mcp cp .env.example .env uv sync uv run python src/main.py ``` Server runs on `http://127.0.0.1:8080/mcp`. # Python Framework Source: https://docs.dedaluslabs.ai/sdk/mcp/python/index Build production MCP clients and servers with the Dedalus Python framework. Python is the recommended production path for the Dedalus MCP Framework today. Source: [dedalus-labs/dedalus-mcp-python](https://github.com/dedalus-labs/dedalus-mcp-python). We believe MCP is the interface between models and the world. To use any tools, we believe it must be: 1. **Secure**: Credentials handled correctly and securely. 2. **Modular**: Tools that compose, instead of locked to one server. 3. **Spec-faithful**: Every behavior traceable. With Dedalus MCP, we achieve all above. We don't bundle CLI scaffolding or opinionated middleware. We integrate with your existing stack. ## What is MCP? Model Context Protocol (MCP) is a standard for AI agents to interact with external services. Instead of hardcoding integrations, you expose **tools** (functions), **resources** (data), and **prompts** (templates) that any MCP-compatible client—Claude, GPT, Cursor—can discover and use. Think of it as a universal API for agent capabilities: you define what's available, the model decides when to use it. ## Why Dedalus ### Security by Default Other frameworks leave authentication as an exercise for the server builder. We built [DAuth](/sdk/mcp/python/authorization): * **Zero-trust**: Dedalus never sees raw API keys or access tokens. * **Hardware enclave**: Credentials are validated momentarily, then zeroed from memory. * **Host-blind**: Credentials are encrypted client-side before leaving your device. * **Intent-based**: Define access intents (e.g., `slack_read`) to prevent permission hijacking. Production-grade auth in a few lines of code, not weeks of infrastructure work. ### Modular Servers Most MCP frameworks couple tools to servers: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from fastmcp import FastMCP mcp = FastMCP(name="CalculatorServer") # Tool is bound to this server instance @mcp.tool def add(a: int, b: int) -> int: return a + b ``` Dedalus decouples decoration from registration: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import tool # @tool attaches metadata. That's it. @tool def add(a: int, b: int) -> int: return a + b # collect() registers. Same tool, multiple servers. server_a.collect(add) server_b.collect(add) ``` Tools become portable units. No global state. Tests stay isolated. Functions work everywhere. ### MCP Observability and Versioning MCP has multiple spec versions with real behavioral differences. Unlike other frameworks, Dedalus MCP tracks exactly which version your client negotiated. ### Fast and Production Ready **122 KB**. FastMCP is 8.6 MB—**70× smaller**. We ship **code**, not dependencies. # Installation Source: https://docs.dedaluslabs.ai/sdk/mcp/python/installation Install Dedalus MCP ## Install ```bash pip theme={"theme":{"light":"github-light","dark":"github-dark"}} pip install dedalus-mcp dedalus-labs ``` ```bash uv theme={"theme":{"light":"github-light","dark":"github-dark"}} uv add dedalus-mcp dedalus-labs ``` ## API Key Dedalus MCP works natively with our SDK. Claim your API key from the [dashboard](https://www.dedaluslabs.ai/dashboard/api-keys) and set it as an environment variable: ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} export DEDALUS_API_KEY="your-api-key" ``` Or use a `.env` file: ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} DEDALUS_API_KEY=your-api-key ``` ## Next Build an MCP server in 5 minutes Framework philosophy and design # Creative Patterns Source: https://docs.dedaluslabs.ai/sdk/mcp/python/patterns Unconventional ways to use MCP primitives MCP's three primitives (tools, resources, prompts) are building blocks. Combine them creatively. ## Sequential Thinking Keep the model on track during complex reasoning. The model calls this tool repeatedly, building a chain of thoughts it can revise or branch. ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dataclasses import dataclass, field from dedalus_mcp import MCPServer, tool @dataclass class ThinkingState: thoughts: list[dict] = field(default_factory=list) branches: dict[str, list[dict]] = field(default_factory=dict) state = ThinkingState() @tool(description="""Step-by-step reasoning with revision support. Use when: - Breaking down complex problems - Planning that might need course correction - Analysis where the full scope isn't clear initially You can revise previous thoughts, branch into alternatives, or extend beyond your initial estimate.""") def think( thought: str, thought_number: int, total_thoughts: int, next_thought_needed: bool, is_revision: bool = False, revises_thought: int | None = None, branch_id: str | None = None, ) -> dict: entry = { "number": thought_number, "thought": thought, "is_revision": is_revision, "revises": revises_thought, } if branch_id: state.branches.setdefault(branch_id, []).append(entry) else: state.thoughts.append(entry) return { "thought_number": thought_number, "total_thoughts": total_thoughts, "next_thought_needed": next_thought_needed, "history_length": len(state.thoughts), "branches": list(state.branches.keys()), } server = MCPServer("reasoning") server.collect(think) ``` The model decides when to think, revise, or branch. You just provide the infrastructure. ## Context Rehydration Persist important context to a database. After model compaction (when context window fills up), fetch it back instantly. ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import MCPServer, tool, resource import json # Could be Redis, SQLite, Postgres, etc. memory_store: dict[str, dict] = {} @tool(description="Save important context for later retrieval") def remember(key: str, content: str, tags: list[str] | None = None) -> dict: memory_store[key] = { "content": content, "tags": tags or [], "saved_at": datetime.now().isoformat(), } return {"saved": key} @tool(description="Retrieve previously saved context") def recall(key: str) -> dict: if key not in memory_store: return {"error": f"No memory for key: {key}"} return memory_store[key] @tool(description="Find memories by tag") def search_memories(tag: str) -> list[dict]: return [ {"key": k, **v} for k, v in memory_store.items() if tag in v.get("tags", []) ] @resource(uri="memory://index", description="All saved memory keys") def memory_index() -> dict: return { "keys": list(memory_store.keys()), "count": len(memory_store), } server = MCPServer("memory") server.collect(remember, recall, search_memories, memory_index) ``` Start conversations with: "Check memory://index for context from previous sessions." ## Live Data Feeds Resources can push updates. Build dashboards, monitoring, or real-time collaboration. ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import MCPServer, resource import asyncio metrics = {"cpu": 0.0, "memory": 0.0, "requests": 0} @resource(uri="system://metrics", description="Live system metrics") def get_metrics() -> dict: return {"timestamp": datetime.now().isoformat(), **metrics} server = MCPServer("monitoring") server.collect(get_metrics) async def update_metrics(): while True: metrics["cpu"] = get_cpu_usage() metrics["memory"] = get_memory_usage() metrics["requests"] = get_request_count() await server.notify_resource_updated("system://metrics") await asyncio.sleep(5) ``` Subscribed clients receive `notifications/resources/updated` when data changes. ## Persona Switching Prompts define behavior. Let users switch the model's persona on demand. ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dataclasses import dataclass from dedalus_mcp import MCPServer, prompt, Message @dataclass(frozen=True) class PersonaArgs: """Arguments for persona prompts.""" context: str | None = None verbosity: str = "normal" focus_area: str | None = None @prompt("persona/architect", description="Senior software architect") def architect_persona(args: PersonaArgs) -> list[Message]: context = f" Current context: {args.context}" if args.context else "" return [ Message(role="assistant", content=f"""You are a senior software architect with 20 years of experience. You think in systems, not features. You ask clarifying questions before proposing solutions. You consider maintainability, scalability, and team dynamics.{context}"""), ] @prompt("persona/reviewer", description="Strict code reviewer") def reviewer_persona(args: PersonaArgs) -> list[Message]: focus = f" Focus especially on {args.focus_area}." if args.focus_area else "" return [ Message(role="assistant", content=f"""You are a meticulous code reviewer. You catch bugs others miss. You insist on tests. You're constructive but don't let things slide.{focus}"""), ] @prompt("persona/rubber-duck", description="Patient debugging companion") def rubber_duck_persona(args: PersonaArgs) -> list[Message]: return [ Message(role="assistant", content="""You help by asking questions, not giving answers. When someone explains their problem, ask what they've tried. Help them think through it systematically."""), ] server = MCPServer("personas") server.collect(architect_persona, reviewer_persona, rubber_duck_persona) ``` Users select `prompts/get persona/architect` to shift behavior mid-conversation. ## Guardrails Tools can validate and constrain model behavior. ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import MCPServer, tool ALLOWED_PATHS = ["/app/data", "/app/config"] BLOCKED_PATTERNS = ["password", "secret", "api_key"] @tool(description="Read a file (with safety checks)") def safe_read(path: str) -> dict: # Path validation if not any(path.startswith(allowed) for allowed in ALLOWED_PATHS): return {"error": f"Access denied: {path}"} content = open(path).read() # Content filtering for pattern in BLOCKED_PATTERNS: if pattern in content.lower(): return {"error": "Content contains sensitive data"} return {"content": content} @tool(description="Execute a command (restricted)") def safe_exec(command: str) -> dict: allowed = ["ls", "cat", "grep", "find"] cmd = command.split()[0] if cmd not in allowed: return {"error": f"Command not allowed: {cmd}"} # Execute safely... return {"output": "..."} ``` The model can only do what you allow. ## Workflow Orchestration Chain tools into multi-step workflows with checkpoints. ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import MCPServer, tool, resource from enum import Enum class WorkflowStatus(Enum): PENDING = "pending" RUNNING = "running" COMPLETED = "completed" FAILED = "failed" workflows: dict[str, dict] = {} @tool(description="Start a new workflow") def start_workflow(workflow_id: str, steps: list[str]) -> dict: workflows[workflow_id] = { "status": WorkflowStatus.RUNNING.value, "steps": steps, "current_step": 0, "results": [], } return {"workflow_id": workflow_id, "status": "started"} @tool(description="Complete current step and advance") def complete_step(workflow_id: str, result: str) -> dict: wf = workflows.get(workflow_id) if not wf: return {"error": "Workflow not found"} wf["results"].append(result) wf["current_step"] += 1 if wf["current_step"] >= len(wf["steps"]): wf["status"] = WorkflowStatus.COMPLETED.value return { "step_completed": wf["current_step"], "next_step": wf["steps"][wf["current_step"]] if wf["current_step"] < len(wf["steps"]) else None, "status": wf["status"], } @resource(uri="workflows://active", description="All active workflows") def active_workflows() -> dict: return { wid: wf for wid, wf in workflows.items() if wf["status"] == WorkflowStatus.RUNNING.value } ``` The model manages complex multi-step processes with clear state. ## Audit Trail Log everything the model does for compliance or debugging. ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import MCPServer, tool, resource from datetime import datetime audit_log: list[dict] = [] def log_action(action: str, details: dict): audit_log.append({ "timestamp": datetime.now().isoformat(), "action": action, **details, }) @tool(description="Perform a sensitive operation") def sensitive_operation(operation: str, target: str) -> dict: log_action("sensitive_operation", {"operation": operation, "target": target}) # ... do the thing ... return {"status": "completed"} @resource(uri="audit://log", description="Complete audit trail") def get_audit_log() -> list[dict]: return audit_log @resource(uri="audit://recent", description="Last 10 actions") def recent_actions() -> list[dict]: return audit_log[-10:] ``` ## Mix and Match The real power is combining patterns: * **Sequential thinking + memory**: Save reasoning chains for later reference * **Guardrails + audit trail**: Log blocked attempts * **Live feeds + workflows**: Monitor workflow progress in real-time * **Personas + prompts**: Context-aware behavior switching Build what your use case needs. # Quickstart Source: https://docs.dedaluslabs.ai/sdk/mcp/python/quickstart Build and deploy an MCP server in 5 minutes. ## Install ```bash pip theme={"theme":{"light":"github-light","dark":"github-dark"}} pip install dedalus-mcp dedalus-labs ``` ```bash uv theme={"theme":{"light":"github-light","dark":"github-dark"}} uv add dedalus-mcp dedalus-labs ``` ## API Key Claim your API key from the [dashboard](https://www.dedaluslabs.ai/dashboard/api-keys) and set it: ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} export DEDALUS_API_KEY="your-api-key" ``` ## Create a server ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} # server.py from dedalus_mcp import MCPServer, tool @tool(description="Add two numbers") def add(a: int, b: int) -> int: return a + b @tool(description="Multiply two numbers") def multiply(a: int, b: int) -> int: return a * b server = MCPServer("calculator") server.collect(add, multiply) if __name__ == "__main__": import asyncio asyncio.run(server.serve()) ``` Run it: ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} python server.py ``` Server starts on `http://127.0.0.1:8000/mcp`. ## Test with a client ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} # client.py from dedalus_mcp.client import MCPClient import asyncio async def main(): client = await MCPClient.connect("http://127.0.0.1:8000/mcp") # List available tools tools = await client.list_tools() print([t.name for t in tools.tools]) # ['add', 'multiply'] # Call a tool result = await client.call_tool("add", {"a": 2, "b": 3}) print(result.content[0].text) # "5" await client.close() asyncio.run(main()) ``` ## Deploy Ready to host your server remotely? Deploy to the Dedalus platform to access it from anywhere, share with others, or monetize your work. Step-by-step deployment walkthrough with screenshots. ## Next Steps Most secure MCP auth framework in the industry. Examples for production servers (e.g., GitHub and Supabase). # Context Source: https://docs.dedaluslabs.ai/sdk/mcp/python/server/context Access MCP request features inside handlers The `Context` object gives you access to request-scoped utilities while a tool/resource/prompt is executing—primarily **logging**, **progress**, and **request metadata**. Use `get_context()` to fetch the active context. **Note**: `get_context()` only works inside an active MCP request handler. Calling it elsewhere raises `LookupError`. ## Get Context ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import get_context, tool @tool(description="Process data with logging") async def process(data: str) -> str: ctx = get_context() await ctx.info("Processing", data={"bytes": len(data)}) return "done" ``` ## Auto-injection (tools + dependencies) In tools (and dependency callables), parameters annotated as `Context` can be **auto-injected** by the framework—no need to call `get_context()` manually: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import Context, tool @tool(description="Same as process(), but Context is injected") async def process_injected(data: str, ctx: Context) -> str: await ctx.info("Processing", data={"bytes": len(data)}) return "done" ``` ## Available features | Feature | API | What it does | | -------------------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------- | | Logging | `ctx.debug()`, `ctx.info()`, `ctx.warning()`, `ctx.error()`, `ctx.log()` | Send log messages to the client | | Request metadata | `ctx.request_id`, `ctx.session_id`, `ctx.progress_token` | Identify the current request/session and progress token | | Server/runtime access | `ctx.server`, `ctx.runtime` | Access runtime wiring (if present) | | Auth context | `ctx.auth_context` | Access the auth context (if authorization is enabled) | | Progress | `ctx.report_progress(...)`, `ctx.progress(...)` | Emit progress notifications (if the client provided a progress token) | | Dispatch (optional) | `ctx.dispatch(...)` | Send authenticated HTTP requests via the configured dispatch backend (if configured) | | Connection resolution (optional) | `ctx.resolve_client(...)` | Resolve a connection handle into a client via the configured resolver (if configured) | ## Request metadata ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import get_context, tool @tool(description="Show request metadata") async def my_tool() -> dict: ctx = get_context() return { "request_id": ctx.request_id, "session_id": ctx.session_id, # may be None (e.g. stdio) "progress_token": ctx.progress_token, # may be None if client didn't request progress } ``` ## Authorization context If authorization is enabled, `ctx.auth_context` may be set; otherwise it's `None`: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import get_context, tool @tool(description="Show auth context presence") async def whoami() -> dict: ctx = get_context() if ctx.auth_context is None: return {"user": "anonymous"} return {"auth_context": "present"} ``` ## Progress example ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} import anyio from dedalus_mcp import get_context, tool @tool(description="Process files with progress reporting") async def process_files(paths: list[str]) -> dict: ctx = get_context() processed = 0 async with ctx.progress(total=len(paths)) as tracker: for _path in paths: await anyio.sleep(0.01) processed += 1 await tracker.advance(1) return {"processed": processed} ``` # Elicitation Source: https://docs.dedaluslabs.ai/sdk/mcp/python/server/elicitation Request user input during tool execution Elicitation lets your server ask the client to collect **structured input from the user** while a tool is running—confirmations, missing parameters, or a small "form". ## Basic usage ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import get_context, tool, types @tool(description="Deploy to environment") async def deploy(env: str) -> str: ctx = get_context() server = ctx.server if server is None: raise RuntimeError("No active server in context") result = await server.request_elicitation( types.ElicitRequestParams( message=f"Deploy to {env}?", requestedSchema={ "type": "object", "properties": { "confirm": {"type": "boolean"}, }, "required": ["confirm"], }, ) ) if result.action == "accept" and result.content and result.content.get("confirm"): return f"Deployed to {env}" return "Deployment cancelled" ``` ## Parameters `request_elicitation(...)` takes an `ElicitRequestParams` with: * **`message: str`**: text shown to the user * **`requestedSchema: dict`**: a restricted JSON Schema object describing the expected fields ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} params = types.ElicitRequestParams( message="Enter configuration", requestedSchema={ "type": "object", "properties": { "name": {"type": "string"}, "replicas": {"type": "integer"}, "dry_run": {"type": "boolean"}, }, "required": ["name"], }, ) result = await ctx.server.request_elicitation(params) ``` **Schema limitations (enforced by Dedalus MCP):** * top-level `type` must be `"object"` * `properties` must be a non-empty object * property `type` must be one of: `"string"`, `"number"`, `"integer"`, `"boolean"` * nested objects/arrays are not supported ## Response actions The client returns an `ElicitResult` with: * `action`: `"accept" | "decline" | "cancel"` * `content`: optional mapping (present when `action == "accept"`) ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} result = await ctx.server.request_elicitation(params) match result.action: case "accept": data = result.content or {} return f"Got: {data}" case "decline": return "User declined" case "cancel": return "User cancelled" ``` ## Example: Progressive disclosure Collect complex information step-by-step: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import get_context, tool, types @tool(description="Create new project") async def create_project() -> str: ctx = get_context() server = ctx.server if server is None: raise RuntimeError("No active server in context") name = await server.request_elicitation( types.ElicitRequestParams( message="Project name:", requestedSchema={ "type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"], }, ) ) if name.action != "accept" or not name.content: return "Cancelled" project_type = await server.request_elicitation( types.ElicitRequestParams( message="Project type (web/api/cli):", requestedSchema={ "type": "object", "properties": {"type": {"type": "string"}}, "required": ["type"], }, ) ) if project_type.action != "accept" or not project_type.content: return "Cancelled" confirm = await server.request_elicitation( types.ElicitRequestParams( message=f"Create {project_type.content['type']} project '{name.content['name']}'?", requestedSchema={ "type": "object", "properties": {"confirm": {"type": "boolean"}}, "required": ["confirm"], }, ) ) if confirm.action == "accept" and confirm.content and confirm.content.get("confirm"): return f"Created {name.content['name']}" return "Cancelled" ``` ## Error handling Elicitation requires an active MCP session and a client that advertises the `elicitation` capability. If not, `request_elicitation(...)` raises `McpError` (typically `METHOD_NOT_FOUND`), and timeouts raise `McpError` as well. ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from mcp.shared.exceptions import McpError from dedalus_mcp import types try: result = await ctx.server.request_elicitation(params) except McpError as e: return f"Elicitation unavailable: {e}" ``` # Logging Source: https://docs.dedaluslabs.ai/sdk/mcp/python/server/logging Send log messages to connected clients Logging lets your server send debug/info/warning/error messages to MCP clients while handling a request. This is helpful for visibility during tool execution and for debugging. **Note**: Clients decide how (or whether) to display these logs. ## Basic usage ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import get_context, tool @tool(description="Process data") async def process(data: str) -> str: ctx = get_context() await ctx.info("Processing", data={"bytes": len(data)}) # ... your work ... await ctx.info("Processing complete") return "done" ``` ## Log levels ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} await ctx.debug("Detailed debugging info") await ctx.info("General operational messages") await ctx.warning("Warning conditions") await ctx.error("Error conditions") ``` | Method | Level | Use case | | --------------- | ------- | ------------------------------ | | `ctx.debug()` | DEBUG | Detailed debugging information | | `ctx.info()` | INFO | General operational messages | | `ctx.warning()` | WARNING | Warning conditions | | `ctx.error()` | ERROR | Error conditions | ## Example: Data pipeline ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import get_context, tool @tool(description="Run data pipeline") async def run_pipeline(source: str) -> dict: ctx = get_context() await ctx.info("Starting pipeline", data={"source": source}) # Load await ctx.debug("Loading data...") data = load_data(source) # your code await ctx.info("Loaded records", data={"count": len(data)}) # Transform await ctx.debug("Transforming data...") try: transformed = transform(data) # your code except ValueError as e: await ctx.warning("Transform warning", data={"error": str(e)}) transformed = fallback_transform(data) # your code # Save await ctx.debug("Saving results...") try: save(transformed) # your code await ctx.info("Pipeline complete", data={"records": len(transformed)}) except OSError as e: await ctx.error("Save failed", data={"error": str(e)}) raise return {"records": len(transformed)} ``` ## Example: Batch processing ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import get_context, tool @tool(description="Process items in batch") async def batch_process(items: list[str]) -> dict: ctx = get_context() results = {"success": 0, "failed": 0} await ctx.info("Starting batch", data={"items": len(items)}) for i, item in enumerate(items, start=1): await ctx.debug("Processing item", data={"index": i, "total": len(items), "item": item}) try: process_item(item) # your code results["success"] += 1 except Exception as e: await ctx.warning("Item failed", data={"item": item, "error": str(e)}) results["failed"] += 1 if results["failed"]: await ctx.warning("Batch completed with failures", data=results) else: await ctx.info("Batch completed successfully", data=results) return results ``` ## Structured logging Pass structured fields using `data=`: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} await ctx.info( "Request processed", data={ "duration_ms": 150, "items_processed": 42, }, ) ``` **Tip**: Avoid using the key `"msg"` inside `data`—Dedalus MCP uses `"msg"` internally for the main message text. # Overview Source: https://docs.dedaluslabs.ai/sdk/mcp/python/server/overview Build MCP servers with dedalus_mcp ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import MCPServer, tool @tool(description="Add two numbers") def add(a: int, b: int) -> int: return a + b server = MCPServer("calculator") server.collect(add) if __name__ == "__main__": import asyncio asyncio.run(server.serve()) ``` Type hints become JSON Schema automatically. Register tools with `collect()`. Same pattern works for resources and prompts. **Server name must match your slug.** The `name` in `MCPServer("my-server")` must match your deployment slug and `ctx.dispatch()` calls. This ensures OAuth callbacks and request routing work correctly. ## With Dedalus SDK MCP integration is trivial. Pass servers directly to `mcp_servers`: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_labs import AsyncDedalus, DedalusRunner client = AsyncDedalus() runner = DedalusRunner(client) # Hosted MCP server (marketplace slug) response = await runner.run( input="Search for authentication docs", model="anthropic/claude-sonnet-4-20250514", mcp_servers=["your-org/your-server"], ) # Local MCP server URL response = await runner.run( input="Search for authentication docs", model="anthropic/claude-sonnet-4-20250514", mcp_servers=["http://localhost:8000/mcp"], ) ``` That's it. The SDK handles connection, tool discovery, and execution. ## Server primitives MCP servers expose three types of capabilities: | Primitive | Control | Description | | ------------------------------------------------- | ---------- | ------------------------------------------ | | [**Tools**](/sdk/mcp/python/server/tools) | Model | Functions the LLM calls during reasoning. | | [**Resources**](/sdk/mcp/python/server/resources) | Model/User | Data the LLM can read for context. | | [**Prompts**](/sdk/mcp/python/server/prompts) | User | Message templates users select and render. | Tools are model-controlled: the LLM decides when to call them. Prompts are user-controlled: users choose which prompt to run. Resources can be either. ## Additional capabilities | Capability | How | | ---------------- | --------------------------------------- | | **Progress** | `ctx.progress()` for long-running tasks | | **Logging** | `ctx.info()`, `ctx.debug()`, etc. | | **Cancellation** | `ctx.cancelled` flag | ## Next Build an MCP server in 5 minutes Expose functions to AI agents Reusable message templates Expose data for agents to read X, GitHub, Gmail production servers # Progress Source: https://docs.dedaluslabs.ai/sdk/mcp/python/server/progress Report progress of long-running operations Progress reporting lets your server send progress updates to the client while a tool is running. Clients can use these updates to show spinners, progress bars, and "still working" UI during slow operations. **Note**: `ctx.report_progress(...)` only sends a notification if the client provided a progress token for this request. If the client didn't request progress, it's a no-op. ## Basic usage ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import get_context, tool @tool(description="Process files") async def process_files(files: list[str]) -> str: ctx = get_context() for i, file in enumerate(files, start=1): await ctx.report_progress(i, total=len(files), message=f"Processing {file}") process_file(file) # your code; if this blocks, offload or make it async return f"Processed {len(files)} files" ``` ## Parameters ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} await ctx.report_progress( progress=50, # current progress value total=100, # optional total (for percentage) message="Halfway done...", # optional status message ) ``` | Parameter | Type | Description | | ---------- | ---------------------- | ------------------------------------------- | | `progress` | `int \| float` | Current progress value | | `total` | `int \| float \| None` | Optional total value; enables percentage UI | | `message` | `str \| None` | Optional human-friendly status text | ## Example: File download ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import get_context, tool @tool(description="Download files") async def download_files(urls: list[str]) -> dict: ctx = get_context() downloaded: list[str] = [] for i, url in enumerate(urls, start=1): await ctx.report_progress(i - 1, total=len(urls), message=f"Downloading {url}") await ctx.info("Downloading", data={"url": url}) path = await download(url) # your code downloaded.append(path) await ctx.report_progress(i, total=len(urls)) return {"files": downloaded} ``` ## Example: Data processing pipeline ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import get_context, tool @tool(description="Process large dataset") async def process_dataset(dataset_id: str) -> dict: ctx = get_context() await ctx.report_progress(0, total=100, message="Loading dataset") data = load_dataset(dataset_id) # your code await ctx.report_progress(30, total=100, message="Transforming data") total_items = len(data) if total_items: for i, item in enumerate(data, start=1): # map item progress into the 30..70 range progress = 30 + int((i / total_items) * 40) await ctx.report_progress(progress, total=100) transform(item) # your code await ctx.report_progress(70, total=100, message="Saving results") save_results(data) # your code await ctx.report_progress(100, total=100, message="Done") return {"processed": len(data)} ``` ## Example: Indeterminate progress If you don't know the total up front, omit `total` and send periodic updates: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import get_context, tool @tool(description="Search until found") async def search(query: str) -> str: ctx = get_context() pages_searched = 0 while True: pages_searched += 1 await ctx.report_progress(pages_searched, message=f"Searched {pages_searched} pages") result = search_page(query, pages_searched) # your code if result: return result if pages_searched > 100: return "Not found" ``` ## Tips * **Prefer async work**: progress updates are most useful when your tool is doing I/O (`async def`). If you do CPU-heavy or blocking work, consider offloading it so progress notifications can still flow. * **Use `message` sparingly**: short messages like "Downloading…", "Transforming…", "Saving…" are easiest for clients to display. * **Don't spam updates**: sending progress on every tiny step can be noisy. For very large loops, you may want to report every N items. # Prompts Source: https://docs.dedaluslabs.ai/sdk/mcp/python/server/prompts Expose reusable message templates to MCP clients with the @prompt decorator Prompts are user-controlled message templates. Unlike tools (which the LLM calls), prompts are selected by users and rendered into conversation messages. Prompts flow through the MCP protocol like this: 1. A client discovers prompts via `prompts/list` (each prompt may include `arguments` and metadata). 2. A client renders a prompt via `prompts/get` with **string-valued** `arguments`. 3. The server executes your prompt renderer. 4. The server returns a `GetPromptResult` containing `messages` (and optional `description`) per MCP spec. Define prompts with `@prompt(...)` and register them with `server.collect(...)` (or inside `with server.binding(): ...`). ### Decorator signature * **`prompt(...)`**: `prompt(name: str, *, description=None, title=None, arguments=None, icons=None, meta=None)` Your renderer is called like: * **`fn(arguments: dict[str, str] | None)`** → returns messages / mapping / `GetPromptResult` / `None` *** ## Basic prompt ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import MCPServer, prompt, types @prompt( "code-review", description="Review code for issues", arguments=[ types.PromptArgument(name="language", required=False), types.PromptArgument(name="focus", required=False), ], ) def code_review(arguments: dict[str, str] | None): args = arguments or {} language = args.get("language", "python") focus = args.get("focus") focus_text = f" Focus on: {focus}." if focus else "" return [ ("assistant", "You are a senior code reviewer."), ("user", f"Review the following {language} code.{focus_text}"), ] server = MCPServer("assistant") server.collect(code_review) ``` The description tells the client/LLM what the prompt is for. `arguments=[...]` defines what the client should pass to `prompts/get`. *** ## Arguments (required vs optional) Dedalus MCP treats arguments as required **only** if you mark them `required=True` in the decorator's `arguments=[...]`. ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import MCPServer, prompt, types @prompt( "translate", description="Translate text", arguments=[ types.PromptArgument(name="text", required=True), types.PromptArgument(name="target_lang", required=False), ], ) def translate(arguments: dict[str, str] | None): args = arguments or {} text = args["text"] target = args.get("target_lang", "English") return [("user", f"Translate this to {target}: {text}")] server = MCPServer("assistant") server.collect(translate) ``` If required args are missing, Dedalus raises an MCP error with `INVALID_PARAMS`. *** ## Complex argument values (lists/dicts) MCP prompt arguments are strings. If you need structured values, pass JSON strings and parse them yourself: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} import json from dedalus_mcp import prompt, types @prompt( "summarize", description="Summarize a document", arguments=[types.PromptArgument(name="focus_areas_json", required=False)], ) def summarize(arguments: dict[str, str] | None): args = arguments or {} focus_areas = json.loads(args.get("focus_areas_json", "[]")) return [("user", f"Summarize this document. Focus on: {focus_areas}")] ``` *** ## Return formats Prompt renderers can return: * **A list/iterable of messages**, where each item can be: * a `(role, content)` tuple * a mapping like `{"role": "...", "content": ...}` * a `PromptMessage` instance * **A mapping** with explicit control: * **required**: `"messages"` * **optional**: `"description"` * **`GetPromptResult`** * **`None`** (produces zero messages) **Not supported**: returning a raw `str` (Dedalus raises `TypeError` so you always provide role + content). ### Explicit control (mapping) ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import prompt @prompt("status", description="Daily status template") def status(arguments: dict[str, str] | None): return { "description": "Status template", "messages": [ ("assistant", "You summarize daily status reports."), ("user", "Write yesterday/today/blockers."), ], } ``` *** ## Message content For message `content`, you can use: * a `str` (auto-coerced to text content) * a full content-block mapping (e.g. `{"type": "text", "text": "..."}`) * a content-block instance from `dedalus_mcp.types` (e.g. `TextContent`, `ImageContent`, etc.) *** ## Async prompts ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import prompt @prompt("db-summary", description="Summarize database state") async def db_summary(arguments: dict[str, str] | None): # await fetch_db_stats(...) return [ ("assistant", "You analyze database metrics."), ("user", "Summarize current DB health and recent anomalies."), ] ``` Prefer `async def` for I/O. *** ## Decorator options ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import prompt @prompt( "analyze", description="Analyze code", title="Code Analysis", icons=[{"type": "url", "url": "https://example.com/icon.png"}], meta={"category": "code"}, arguments=[{"name": "language", "required": False}], ) def analyze(arguments: dict[str, str] | None): language = (arguments or {}).get("language", "python") return [("user", f"Analyze this {language} code for bugs, style, and security.")] ``` *** ## Context access If a prompt is rendered during an MCP request, it can access context via `get_context()` (for logging, progress, etc.). **Note**: `get_context()` only works inside an active MCP request handler; calling it outside a request raises `LookupError`. ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import prompt, get_context @prompt("generate-report", description="Generate a report request") async def generate_report(arguments: dict[str, str] | None): ctx = get_context() await ctx.info("Rendering prompt", data={"args": arguments or {}}) return [("user", "Generate a concise weekly report for this project.")] ``` *** ## Testing Test prompt renderers like normal functions: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} def test_code_review_prompt(): msgs = code_review({"language": "python", "focus": "error handling"}) assert len(msgs) == 2 ``` Integration-style test via the server API (mirrors `prompts/get`): ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} import pytest from dedalus_mcp import MCPServer @pytest.mark.asyncio async def test_prompt_rendering(): server = MCPServer("test") server.collect(code_review) result = await server.invoke_prompt("code-review", arguments={"language": "rust"}) assert result.messages ``` # Resources Source: https://docs.dedaluslabs.ai/sdk/mcp/python/server/resources Expose data for AI agents to read with the @resource decorator Resources are **read-only data** the client/LLM can pull in as context. Unlike tools (actions), resources are meant to provide information **without side effects**. Clients can also subscribe to resource URIs and receive change notifications. Resources flow through the MCP protocol like this: 1. A client discovers resources via `resources/list` (each resource includes `uri`, optional `name`, optional `mimeType`, etc.). 2. A client reads a resource via `resources/read` with a `uri`. 3. The server executes your resource handler. 4. The server returns a `ReadResourceResult` containing `contents` per MCP spec. Define resources with `@resource(...)` and register them with `server.collect(...)` (or inside `with server.binding(): ...`). ### Decorator signature * **`resource(...)`**: `resource(uri: str, *, name=None, description=None, mime_type=None)` Your handler is called like: * **`fn()`** → returns `str` (text) or `bytes` (binary). (`async def` is also supported.) *** ## Basic resource ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import MCPServer, resource @resource("resource://config/app", name="config", description="Application config", mime_type="application/json") def app_config() -> str: return '{"debug": true, "version": "1.2.0"}' server = MCPServer("config-server") server.collect(app_config) ``` The decorator defines the resource URI. `collect()` registers it. Clients list resources with `resources/list` and read them with `resources/read`. *** ## Text vs binary Return `str` for text: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import resource @resource("resource://docs/readme", mime_type="text/markdown") def readme() -> str: with open("README.md", "r", encoding="utf-8") as f: return f.read() ``` Return `bytes` for binary (Dedalus encodes it as base64 in the MCP response): ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import resource @resource("resource://assets/logo", mime_type="image/png") def logo() -> bytes: with open("logo.png", "rb") as f: return f.read() ``` *** ## Async resources ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} import anyio from dedalus_mcp import resource @resource("resource://api/users", description="Active users", mime_type="application/json") async def users() -> str: await anyio.sleep(0.1) # simulate I/O return '{"users": ["ada", "grace"]}' ``` Prefer `async def` for I/O. (Like tools, sync handlers run inline.) *** ## MIME types ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} import json from dedalus_mcp import resource @resource("resource://data/report", mime_type="application/json") def report() -> str: return json.dumps({"users": 100, "active": 42}) ``` Common defaults: * `text/plain` (default when returning `str` and no `mime_type` is provided) * `application/octet-stream` (default when returning `bytes` and no `mime_type` is provided) *** ## Subscriptions Clients can subscribe to resource changes via `resources/subscribe` and unsubscribe via `resources/unsubscribe`. When your underlying data changes, notify subscribers: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import MCPServer server = MCPServer("live-data") server.collect(users) # When the data behind this URI changes: await server.notify_resource_updated("resource://api/users") ``` Subscribed clients receive `notifications/resources/updated`. *** ## Decorator options ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import resource @resource( "resource://config/app", name="app-config", # Human-friendly name (shown in resources/list) description="App settings", # Shown to clients mime_type="application/json", ) def config() -> str: return '{"debug": false}' ``` *** ## Error handling If your resource handler raises, Dedalus returns a **text fallback** resource: * `mimeType="text/plain"` * `text="Resource error: "` If a URI is not registered, `resources/read` returns an empty `contents` list. *** ## Testing Test resource handlers as normal functions: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} import json def test_config_resource(): data = json.loads(app_config()) assert data["version"] == "1.2.0" ``` Integration-style test via the server API (mirrors `resources/read`): ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} import pytest from dedalus_mcp import MCPServer @pytest.mark.asyncio async def test_resource_read(): server = MCPServer("test") server.collect(app_config) result = await server.invoke_resource("resource://config/app") assert result.contents[0].text == '{"debug": true, "version": "1.2.0"}' ``` *** ## Resource templates Use `@resource_template(...)` to advertise **URI patterns** via `resources/templates/list` (for client discovery and completions). Dedalus MCP currently registers templates for listing, but **does not automatically route `resources/read` to a template function**—you still need to register concrete resource URIs with `@resource(...)`. ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import resource_template @resource_template( "user-profile", uri_template="resource://users/{user_id}", description="User profile by ID", ) async def user_profile(user_id: str) -> str: user = await fetch_user(user_id) return json.dumps(user) ``` # Roots Source: https://docs.dedaluslabs.ai/sdk/mcp/python/server/roots Access client filesystem boundaries Roots are filesystem boundaries **advertised by the client**. Servers can use roots to understand what parts of the client's filesystem are intended to be in-scope (for example, "this project folder"), and to **enforce guardrails** when reading or writing files. In MCP, Roots are a **client capability** (`roots/list`). Dedalus MCP provides a server-side `RootsService` that: * fetches roots from the client, * caches a per-session snapshot, and * offers a `RootGuard` helper for path checks. ## Basic usage Fetch the latest roots for the current session, then use them: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import get_context, tool @tool(description="List client roots") async def list_roots() -> list[str]: ctx = get_context() server = ctx.server if server is None: raise RuntimeError("No active server in context") roots = await server.roots.refresh(ctx.session) # fetch from client return [f"{r.name}: {r.uri}" for r in roots] ``` You don't need to re-fetch every time, you can read the cached snapshot: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} roots = ctx.server.roots.snapshot(ctx.session) ``` *** ## Root structure Each root contains: | Field | Type | Description | | ------ | ----- | ------------------------------------ | | `uri` | `str` | Root URI (typically a `file://` URI) | | `name` | `str` | Human-readable name | *** ## Example: Safe file operations (RootGuard) Use `RootGuard` to check whether a path is inside one of the allowed roots: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from pathlib import Path from dedalus_mcp import get_context, tool @tool(description="Read a file, restricted to roots") async def safe_read(filepath: str) -> str: ctx = get_context() server = ctx.server if server is None: raise RuntimeError("No active server in context") # Make sure we have an up-to-date snapshot await server.roots.refresh(ctx.session) guard = server.roots.guard(ctx.session) target = Path(filepath).expanduser().resolve() if not guard.within(target): raise ValueError("Path is outside allowed roots") return target.read_text(encoding="utf-8") ``` *** ## Example: Project discovery (file:// roots) If your client roots are `file://...` URIs, you can walk them to discover projects. ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from pathlib import Path from urllib.parse import urlparse, unquote from dedalus_mcp import get_context, tool def file_uri_to_path(uri: str) -> Path: parsed = urlparse(uri) if parsed.scheme != "file": raise ValueError(f"Unsupported root scheme: {parsed.scheme!r}") return Path(unquote(parsed.path)).expanduser().resolve() @tool(description="Find project roots by marker files") async def find_projects() -> list[dict]: ctx = get_context() server = ctx.server if server is None: raise RuntimeError("No active server in context") roots = await server.roots.refresh(ctx.session) projects: list[dict] = [] for root in roots: root_path = file_uri_to_path(str(root.uri)) for marker in ["package.json", "pyproject.toml", "Cargo.toml"]: if (root_path / marker).exists(): projects.append( { "root": root.name, "path": str(root_path), "type": marker, } ) return projects ``` *** ## Example: Scoped search Search only within roots (and log what you're doing): ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from pathlib import Path from urllib.parse import urlparse, unquote from dedalus_mcp import get_context, tool def file_uri_to_path(uri: str) -> Path: parsed = urlparse(uri) if parsed.scheme != "file": raise ValueError(f"Unsupported root scheme: {parsed.scheme!r}") return Path(unquote(parsed.path)).expanduser().resolve() @tool(description="Search for files within roots") async def search_files(pattern: str) -> list[str]: ctx = get_context() server = ctx.server if server is None: raise RuntimeError("No active server in context") roots = await server.roots.refresh(ctx.session) await ctx.info("Searching roots", data={"roots": len(roots), "pattern": pattern}) matches: list[str] = [] for root in roots: await ctx.debug("Searching root", data={"root": root.name, "uri": str(root.uri)}) root_path = file_uri_to_path(str(root.uri)) for match in root_path.rglob(pattern): matches.append(str(match)) await ctx.info("Search complete", data={"matches": len(matches)}) return matches ``` *** ## Notes * **Caching**: `server.roots.snapshot(session)` returns the cached roots. `await server.roots.refresh(session)` updates the cache by calling the client. * **Client-driven updates**: If the client sends `roots/list_changed`, Dedalus MCP updates the snapshot (debounced) for that session automatically. * **Security**: Roots are guidance + a boundary for your own checks. If you're doing file I/O, always enforce a guard (`RootGuard.within(...)`) before reading/writing. # Sampling Source: https://docs.dedaluslabs.ai/sdk/mcp/python/server/sampling Request LLM completions from the client Sampling lets your server ask the client to run an LLM and return a completion **while a tool is executing**. This enables tools to leverage AI for analysis and generation without the client having to orchestrate multiple tool calls. ## Basic usage ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import get_context, tool, types @tool(description="Analyze data with Guassian assumptions") async def analyze(data: str) -> str: ctx = get_context() params = types.CreateMessageRequestParams( messages=[ types.SamplingMessage( role="user", content=types.TextContent(type="text", text=f"Analyze this data with Guassian assumptions and expose the estimators: {data}"), ) ], maxTokens=400, ) result = await ctx.server.request_sampling(params) return result.content.text ``` ## Parameters Sampling requests are expressed as `CreateMessageRequestParams` (field names match the MCP schema, e.g. `maxTokens`, `systemPrompt`). ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} params = types.CreateMessageRequestParams( messages=[ types.SamplingMessage( role="user", content=types.TextContent(type="text", text="Analyze this data"), ) ], systemPrompt="You are an expert analyst", temperature=0.7, # 0.0 = deterministic, 1.0 = creative maxTokens=1024, # maximum output tokens ) result = await ctx.server.request_sampling(params) ``` | Parameter | Type | Description | | ------------------ | ------------------------------------------------ | ---------------------------------------------------------- | | `messages` | `list[SamplingMessage]` | Prompt or conversation messages | | `systemPrompt` | `str \| None` | Instructions for the LLM | | `temperature` | `float \| None` | Randomness/creativity | | `maxTokens` | `int` | Maximum output tokens (**required**) | | `model` | `str \| None` | Optional model hint | | `stopSequences` | `list[str] \| None` | Stop strings | | `includeContext` | `"none" \| "thisServer" \| "allServers" \| None` | Whether the client should include additional context | | `modelPreferences` | `ModelPreferences \| None` | Model selection hints (client may ignore) | | `metadata` | `dict[str, object] \| None` | Opaque metadata; Dedalus will add a `requestId` if missing | ## Response `request_sampling(...)` returns a `CreateMessageResult`. Most clients return `TextContent`: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} result = await ctx.server.request_sampling(params) print(result.content.text) ``` ## Multi-turn conversations Pass a list of messages for multi-turn context: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import types params = types.CreateMessageRequestParams( messages=[ types.SamplingMessage(role="user", content=types.TextContent(type="text", text="What is Python?")), types.SamplingMessage(role="assistant", content=types.TextContent(type="text", text="A programming language.")), types.SamplingMessage(role="user", content=types.TextContent(type="text", text="What are its main features?")), ], maxTokens=200, ) result = await ctx.server.request_sampling(params) ``` ## Example: Code review ````python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import get_context, tool, types @tool(description="Review code for issues in the repo") async def review_code(code: str, language: str) -> str: ctx = get_context() params = types.CreateMessageRequestParams( messages=[ types.SamplingMessage( role="user", content=types.TextContent( type="text", text=f"Review this {language} code:\n\n```{language}\n{code}\n```", ), ) ], systemPrompt="You are an expert code reviewer. Be concise and actionable.", temperature=0.2, maxTokens=500, ) result = await ctx.server.request_sampling(params) return result.content.text ```` ## Error handling Sampling requires the client to advertise the sampling capability. If the client doesn't support sampling, `request_sampling(...)` raises `McpError` (typically `METHOD_NOT_FOUND`): ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from mcp.shared.exceptions import McpError from dedalus_mcp import get_context, tool, types @tool(description="Analyze with AI with Guassian assumption") async def analyze_with_fallback(data: str) -> str: ctx = get_context() params = types.CreateMessageRequestParams( messages=[types.SamplingMessage(role="user", content=types.TextContent(type="text", text=f"Analyze: {data}"))], maxTokens=256, ) try: result = await ctx.server.request_sampling(params) return result.content.text except McpError as e: return f"Sampling unavailable: {e}" ``` # Tools Source: https://docs.dedaluslabs.ai/sdk/mcp/python/server/tools Expose functions to AI agents with the @tool decorator Tools let agents call your Python functions. Decorate, register, serve. Tools are the core building blocks that allow an MCP client to invoke your Python functions via the MCP protocol: 1. A client discovers tools via `tools/list` (each tool includes `inputSchema` and optional `outputSchema`). 2. A client calls a tool via `tools/call` with `arguments` matching the schema. 3. The server executes your callable. 4. The server returns a `CallToolResult` containing `content` (and optionally `structuredContent`) per MCP Spec. ## Basic tool ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import MCPServer, tool @tool(description="Add two numbers") def add(a: int, b: int) -> int: return a + b server = MCPServer("math") server.collect(add) ``` The description tells the LLM what the tool does. Type hints become JSON Schema. ## Async tools ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} import anyio from dedalus_mcp import tool @tool(description="Fetch user data (simulated I/O)") async def get_user(user_id: str) -> dict: await anyio.sleep(0.1) return {"user_id": user_id, "status": "ok"} ``` Prefer async for I/O. **Important**: in Dedalus MCP, **sync tools run inline** (they are not automatically moved to a thread pool). If you need concurrency for blocking work, use `async def` and offload explicitly. ## Type inference Type hints become JSON Schema automatically: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from typing import Literal from pydantic import BaseModel from dedalus_mcp import tool class SearchFilters(BaseModel): category: str | None = None min_price: float = 0.0 @tool(description="Search products") def search( query: str, limit: int = 10, sort: Literal["relevance", "price", "date"] = "relevance", filters: SearchFilters | None = None, ) -> list[dict]: return [{"query": query, "limit": limit, "sort": sort, "filters": filters.model_dump() if filters else None}] ``` Supported: primitives, `list`, `dict`, `Literal`, `Enum`, optionals/unions, Pydantic models, dataclasses, nested models. Required parameters have no default. Optional parameters have one. ## Decorator options ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import tool @tool( name="find_products", # Override tool name description="Search catalog", # Tool description tags={"search", "catalog"}, # For filtering/metadata ) def search_products_impl(query: str) -> list[dict]: return [{"id": "p_1", "name": "Widget", "query": query}] ``` ## Structured returns Return JSON-serializable values: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import tool @tool(description="Analyze text") def analyze(text: str) -> dict: return {"word_count": len(text.split()), "char_count": len(text)} ``` For explicit control, return `CallToolResult`: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import tool from dedalus_mcp.types import CallToolResult, TextContent @tool(description="Custom result") def custom() -> CallToolResult: return CallToolResult( content=[TextContent(type="text", text="Custom message")], isError=False, ) ``` ## Context access Logging and progress via `get_context()`: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} import anyio from dedalus_mcp import tool, get_context @tool(description="Process files with progress reporting") async def process_files(paths: list[str]) -> dict: ctx = get_context() await ctx.info("Starting", data={"count": len(paths)}) processed = 0 try: async with ctx.progress(total=len(paths)) as tracker: for path in paths: # Simulate work; cancellation is delivered as task cancellation await anyio.sleep(0.01) processed += 1 await tracker.advance(1) except anyio.get_cancelled_exc_class(): await ctx.warning("Cancelled", data={"processed": processed}) raise return {"processed": processed} ``` ## Allow-lists Restrict visible tools: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import MCPServer, tool @tool(description="Add") def add(a: int, b: int) -> int: return a + b @tool(description="Multiply") def multiply(a: int, b: int) -> int: return a * b server = MCPServer("gated") server.collect(add, multiply) server.allow_tools({"add"}) ``` Calling a hidden tool returns an error `CallToolResult` indicating the tool is not available. ## Error handling Raise exceptions normally: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_mcp import tool @tool(description="Divide") def divide(a: float, b: float) -> float: if b == 0: raise ValueError("Cannot divide by zero") return a / b ``` ## Testing Test tools as normal functions: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} def test_add(): assert add(2, 3) == 5 ``` For tools using context, test the core logic separately (or use an integration-style harness). # Testing Source: https://docs.dedaluslabs.ai/sdk/mcp/python/testing Test MCP servers ## Unit test tools directly Tools are just functions. Test them without a server: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} def test_add(): assert add(2, 3) == 5 async def test_async_tool(): result = await fetch_user("123") assert result["id"] == "123" ``` ## Tools using context Tools that call `get_context()` require an active request. Test them via integration tests with a real server, or structure your tool to make the context-dependent part mockable: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} @tool(description="Process with logging") async def process(data: str) -> dict: ctx = get_context() await ctx.info("Processing") return do_work(data) # Test do_work separately def test_do_work(): assert do_work("input") == {"result": "output"} ``` ## Integration test with MCPClient Test the full server: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} import pytest from dedalus_mcp import MCPServer, tool from dedalus_mcp.client import MCPClient @tool(description="Add") def add(a: int, b: int) -> int: return a + b @pytest.fixture async def server(): server = MCPServer("test") server.collect(add) # Start server in background task = asyncio.create_task(server.serve()) await asyncio.sleep(0.1) # Let it start yield server task.cancel() @pytest.fixture async def client(server): client = await MCPClient.connect("http://127.0.0.1:8000/mcp") yield client await client.close() async def test_call_tool(client): result = await client.call_tool("add", {"a": 2, "b": 3}) assert result.content[0].text == "5" ``` ## Test registration Verify tools register correctly: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} def test_registration(): server = MCPServer("test") server.collect(add, multiply) names = list(server.tool_names) assert "add" in names assert "multiply" in names ``` ## Isolation Since decorators don't bind to servers at import time, each test gets clean state: ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} def test_a(): server = MCPServer("test-a") server.collect(tool_a) # No teardown needed def test_b(): server = MCPServer("test-b") server.collect(tool_b) # Completely independent ``` # TypeScript Framework Source: https://docs.dedaluslabs.ai/sdk/mcp/typescript/index Alpha MCP framework for TypeScript. The TypeScript MCP framework is in alpha and is not ready for production use cases. Use the Python framework for production MCP servers today. The TypeScript framework is under active development in [dedalus-labs/dedalus-mcp-typescript](https://github.com/dedalus-labs/dedalus-mcp-typescript). Treat the API surface as unstable until the framework leaves alpha. Track TypeScript framework development. Use the production-ready framework today. # Agents API Source: https://docs.dedaluslabs.ai/api-reference/api Unified API for chat completions, embeddings, audio, and image generation across multiple AI providers ## Libraries
## Getting Started Sign up at the [Dedalus Dashboard](https://www.dedaluslabs.ai/dashboard/api-keys) and create an API key. Pick a language from the **Install** tabs above. Send a chat completion request using any supported model. ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} import DedalusLabs from "dedalus-labs"; const client = new DedalusLabs(); const completion = await client.chat.completions.create({ model: "openai/gpt-4o", messages: [{ role: "user", content: "Hello!" }], }); ``` ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_labs import DedalusLabs client = DedalusLabs() completion = client.chat.completions.create( model="openai/gpt-4o", messages=[{"role": "user", "content": "Hello!"}], ) ``` ```bash CLI theme={"theme":{"light":"github-light","dark":"github-dark"}} curl -X POST https://api.dedaluslabs.ai/v1/chat/completions \ -H "Authorization: Bearer $DEDALUS_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model": "openai/gpt-4o", "messages": [{"role": "user", "content": "Hello!"}]}' ``` ## Authentication All endpoints require a Bearer token or `X-API-Key` header. Get your key from the [Dedalus Dashboard](https://www.dedaluslabs.ai/dashboard/api-keys). ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} Authorization: Bearer YOUR_API_KEY ``` ## Chat & Embeddings | Method | Path | Description | | ------ | ---------------------- | -------------------------------------------------------- | | `POST` | `/v1/chat/completions` | [Create chat completion](/api/v1/create-chat-completion) | | `POST` | `/v1/embeddings` | [Create embeddings](/api/v1/create-embeddings) | ## Audio | Method | Path | Description | | ------ | -------------------------- | ---------------------------------------------------- | | `POST` | `/v1/audio/speech` | [Create speech](/api/v1/create-speech) | | `POST` | `/v1/audio/transcriptions` | [Create transcription](/api/v1/create-transcription) | | `POST` | `/v1/audio/translations` | [Create translation](/api/v1/create-translation) | ## Images & Documents | Method | Path | Description | | ------ | ------------------------ | ------------------------------------ | | `POST` | `/v1/images/generations` | [Create image](/api/v1/create-image) | | `POST` | `/v1/ocr` | [OCR](/api-reference/ocr) | ## Models | Method | Path | Description | | ------ | ------------ | ------------------------------- | | `GET` | `/v1/models` | [List models](/api/list-models) | # OCR Source: https://docs.dedaluslabs.ai/api-reference/ocr POST /v1/ocr Extract text from PDFs and images ## Overview The OCR endpoint extracts text from documents and images, returning clean markdown. Powered by Mistral's OCR model. **Supported formats:** PDF, PNG, JPEG, WebP ## Quick Start ```bash CLI theme={"theme":{"light":"github-light","dark":"github-dark"}} curl -X POST https://api.dedaluslabs.ai/v1/ocr \ -H "Authorization: Bearer $DEDALUS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "mistral-ocr-latest", "document": { "type": "document_url", "document_url": "https://arxiv.org/pdf/1706.03762" } }' ``` ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} import httpx import os response = httpx.post( "https://api.dedaluslabs.ai/v1/ocr", headers={"Authorization": f"Bearer {os.environ['DEDALUS_API_KEY']}"}, json={ "model": "mistral-ocr-latest", "document": { "type": "document_url", "document_url": "https://arxiv.org/pdf/1706.03762" } }, timeout=120.0 ) for page in response.json()["pages"]: print(f"Page {page['index']}:\n{page['markdown'][:200]}...") ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} const response = await fetch("https://api.dedaluslabs.ai/v1/ocr", { method: "POST", headers: { Authorization: `Bearer ${process.env.DEDALUS_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ model: "mistral-ocr-latest", document: { type: "document_url", document_url: "https://arxiv.org/pdf/1706.03762", }, }), }); const data = await response.json(); for (const page of data.pages) { console.log(`Page ${page.index}:\n${page.markdown.slice(0, 200)}...`); } ``` For local files, encode as base64 data URI: `data:application/pdf;base64, {base64_data}` ## Response ```json theme={"theme":{"light":"github-light","dark":"github-dark"}} { "pages": [ { "index": 0, "markdown": "# Attention Is All You Need\n\nAshish Vaswani, Noam Shazeer...\n\n# Abstract\n\nThe dominant sequence transduction models..." }, { "index": 1, "markdown": "## 1 Introduction\n\nRecurrent neural networks..." } ], "model": "mistral-ocr-latest" } ``` ## Use Cases ### Invoice Processing Extract line items, totals, and dates from invoices for automated bookkeeping. ### Receipt Scanning Parse receipts for expense tracking—amounts, vendors, dates extracted as structured text. ### Document Digitization Convert scanned documents to searchable, editable markdown while preserving tables and formatting. ## Parameters | Parameter | Type | Required | Description | | ----------------------- | ------ | -------- | ---------------------------------------- | | `model` | string | No | OCR model. Default: `mistral-ocr-latest` | | `document.type` | string | Yes | Always `document_url` | | `document.document_url` | string | Yes | HTTPS URL or data URI | ## Limits * **Max file size:** 50 MB * **Max pages:** 1,000 per document * **Timeout:** 120 seconds # Response Schemas Source: https://docs.dedaluslabs.ai/api-reference/schemas Reference for all API response objects and their structure This page documents all response schemas returned by the Dedalus API. All responses follow OpenAI-compatible formats. *** ## Dedalus Runner Response object returned by the `DedalusRunner` for non-streaming tool execution runs. Final text output from the conversation after all tool executions complete List of all tool execution results from the run Name of the tool that was executed The result returned by the tool execution The step number when this tool was executed Error message if the tool execution failed Total number of steps (LLM calls) used during the run List of tool names that were called during the run Full conversation history including system prompts, user messages, assistant responses, and tool calls/results. Useful for debugging, logging, or continuing conversations. Optional list of detected intents (when `return_intent=true`) Alias for `final_output` (legacy compatibility) Alias for `final_output` (legacy compatibility) Returns a copy of the full conversation history (`messages`) for use in follow-up runs. Enables multi-turn conversations by passing the result to subsequent `runner.run()` calls. ```python Example theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_labs import Dedalus, DedalusRunner client = Dedalus(api_key="YOUR_API_KEY") runner = DedalusRunner(client) def get_weather(location: str) -> str: """Get the current weather for a location.""" return f"The weather in {location} is sunny and 72°F" result = runner.run( input="What's the weather like in San Francisco?", tools=[get_weather], model="openai/gpt-5-nano", max_steps=5 ) # Access result properties print(result.final_output) # "The weather in San Francisco is sunny and 72°F" print(result.steps_used) # e.g., 2 print(result.tools_called) # ["get_weather"] print(result.tool_results) # [{"name": "get_weather", "result": "The weather...", "step": 1}] ``` ```python Accessing Message History theme={"theme":{"light":"github-light","dark":"github-dark"}} import json # Print the full conversation history for msg in result.messages: role = msg.get("role") content = msg.get("content", "") if role == "user": print(f"User: {content}") elif role == "assistant": if msg.get("tool_calls"): tools = [tc["function"]["name"] for tc in msg["tool_calls"]] print(f"Assistant: [calling {', '.join(tools)}]") else: print(f"Assistant: {content}") elif role == "tool": print(f"Tool Result: {content[:100]}...") # Store message history to JSON for logging/debugging with open("conversation_log.json", "w") as f: json.dump(result.messages, f, indent=2) # Continue the conversation with message history follow_up = runner.run( messages=result.to_input_list(), # Pass previous conversation input="What about New York?", # Add new user message tools=[get_weather], model="openai/gpt-5-nano" ) ``` ```json Example Response theme={"theme":{"light":"github-light","dark":"github-dark"}} { "final_output": "The weather in San Francisco is sunny and 72°F", "tool_results": [ { "name": "get_weather", "result": "The weather in San Francisco is sunny and 72°F", "step": 1 } ], "steps_used": 2, "tools_called": ["get_weather"], "messages": [ { "role": "user", "content": "What's the weather like in San Francisco?" }, { "role": "assistant", "tool_calls": [ { "id": "call_abc123", "type": "function", "function": { "name": "get_weather", "arguments": "{\"location\": \"San Francisco\"}" } } ] }, { "role": "tool", "tool_call_id": "call_abc123", "content": "The weather in San Francisco is sunny and 72°F" }, { "role": "assistant", "content": "The weather in San Francisco is sunny and 72°F" } ], "intents": null } ``` *** ## Chat Completions The complete response object for non-streaming chat completions. Unique identifier for the chat completion Object type, always `chat.completion` Unix timestamp (seconds) when the completion was created The model used for completion (e.g., `openai/gpt-5-nano`) List of completion choices Index of this choice The generated message Role of the message author (`assistant`, `tool`, etc.) The content of the message Tool calls requested by the model (present when `finish_reason` is `tool_calls`) Unique identifier for this tool call, referenced when returning results via `tool_call_id` Always `function` Name of the function the model wants to invoke JSON-encoded string of the arguments. Must be parsed with `JSON.parse` / `json.loads` before use. Why the generation stopped: `stop`, `length`, `tool_calls`, `content_filter` Log probability information for tokens Token usage statistics Number of tokens in the prompt Number of tokens in the completion Total tokens used (prompt + completion) System fingerprint for reproducibility ```json Example theme={"theme":{"light":"github-light","dark":"github-dark"}} { "id": "chatcmpl-abc123", "object": "chat.completion", "created": 1677652288, "model": "openai/gpt-5-nano", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "Hello! I'm doing well, thank you for asking." }, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 13, "completion_tokens": 12, "total_tokens": 25 } } ``` *** Streamed response chunks for streaming completions (`stream=true`). Unique identifier for the chat completion Object type, always `chat.completion.chunk` Unix timestamp when the chunk was created The model being used List of chunk choices Index of this choice Incremental content delta Role (only in first chunk) Incremental content string Incremental tool call updates Reason for completion (only in final chunk): `stop`, `length`, `tool_calls`, `content_filter`, or `null` ```json Example theme={"theme":{"light":"github-light","dark":"github-dark"}} { "id": "chatcmpl-abc123", "object": "chat.completion.chunk", "created": 1677652288, "model": "openai/gpt-5-nano", "choices": [ { "index": 0, "delta": { "content": "Hello" }, "finish_reason": null } ] } ``` *** ## Embeddings Response object for embedding creation requests. Object type, always `list` List of embedding objects Object type, always `embedding` The embedding vector (array of floats) Index of this embedding The model used to generate embeddings Token usage information Number of tokens in the input Total tokens processed ```json Example theme={"theme":{"light":"github-light","dark":"github-dark"}} { "object": "list", "data": [ { "object": "embedding", "embedding": [0.0023064255, -0.009327292, -0.0028842222], "index": 0 } ], "model": "openai/text-embedding-3-small", "usage": { "prompt_tokens": 8, "total_tokens": 8 } } ``` *** ## Models Response object for listing available models. Includes rich metadata about capabilities and routing. Object type, always `list` List of model objects Model identifier with provider prefix (e.g., `openai/gpt-4o`, `anthropic/claude-opus-4-5`) Provider name: `openai`, `anthropic`, `google`, `xai`, `deepseek`, `mistral`, etc. ISO 8601 timestamp when the model was created Human-readable display name (optional) Model description (optional) Model capabilities Supports text generation via chat completions Supports image input / multimodal Can generate images Supports audio input/output Supports tool/function calling Supports structured JSON output Supports streaming responses Supports extended reasoning (e.g., o1, o3, Claude thinking) Maximum input context window in tokens Maximum output tokens Provider-specific metadata Model status: `enabled`, `disabled`, `preview`, `deprecated` Which upstream API this model uses (e.g., `openai/chat/completions`, `anthropic/messages`) ```json Example theme={"theme":{"light":"github-light","dark":"github-dark"}} { "object": "list", "data": [ { "id": "openai/gpt-4o", "provider": "openai", "created_at": "1970-01-01T00:00:00Z", "display_name": null, "description": null, "capabilities": { "text": true, "vision": null, "image_generation": null, "audio": null, "tools": null, "structured_output": null, "streaming": null, "thinking": null, "input_token_limit": null, "output_token_limit": null }, "provider_info": { "status": "enabled", "upstream_api": "openai/chat/completions" } }, { "id": "openai/o1", "provider": "openai", "created_at": "1970-01-01T00:00:00Z", "capabilities": { "text": true, "thinking": true }, "provider_info": { "status": "enabled", "upstream_api": "openai/chat/completions" } }, { "id": "anthropic/claude-opus-4-5", "provider": "anthropic", "created_at": "1970-01-01T00:00:00Z", "capabilities": { "text": true, "vision": true, "tools": true }, "provider_info": { "status": "enabled", "upstream_api": "anthropic/messages" } } ] } ``` *** ## Images Response object for image generation requests. Unix timestamp when the images were generated List of generated image objects URL of the generated image (when `response_format="url"`) Base64-encoded image data (when `response_format="b64_json"`) The revised prompt used to generate the image (may differ from input for safety) ```json Example theme={"theme":{"light":"github-light","dark":"github-dark"}} { "created": 1677652288, "data": [ { "url": "https://images.example.com/abc123.png", "revised_prompt": "A cute baby sea otter floating on its back in calm blue water" } ] } ``` *** ## Audio Response object for audio transcription requests. The transcribed text from the audio file ```json Example theme={"theme":{"light":"github-light","dark":"github-dark"}} { "text": "Hello, this is a test of audio transcription." } ``` *** Response object for audio translation requests (always translates to English). The translated text from the audio file (in English) ```json Example theme={"theme":{"light":"github-light","dark":"github-dark"}} { "text": "Hello, this is a test of audio translation." } ``` *** ## Errors All endpoints may return errors with this structure. Error information object Human-readable error message Error type: `invalid_request_error`, `authentication_error`, `rate_limit_error`, `server_error` Specific error code for programmatic handling Parameter that caused the error (if applicable) ```json Example theme={"theme":{"light":"github-light","dark":"github-dark"}} { "error": { "message": "Invalid API key provided", "type": "authentication_error", "code": "invalid_api_key" } } ``` # Create Chat Completion Source: https://docs.dedaluslabs.ai/api/v1/create-chat-completion /openapi.json post /v1/chat/completions Create a chat completion. Generates a model response for the given conversation and configuration. Supports OpenAI-compatible parameters and provider-specific extensions. Headers: - Authorization: bearer key for the calling account. - X-Provider / X-Provider-Key: optional headers for using your own provider API key. Behavior: - If multiple models are supplied, the first one is used, and the agent may hand off to another model. - Tools may be invoked on the server or signaled for the client to run. - Streaming responses emit incremental deltas; non-streaming returns a single object. - Usage metrics are computed when available and returned in the response. Responses: - 200 OK: JSON completion object with choices, message content, and usage. - 400 Bad Request: validation error. - 401 Unauthorized: authentication failed. - 402 Payment Required or 429 Too Many Requests: quota, balance, or rate limit issue. - 500 Internal Server Error: unexpected failure. Billing: - Token usage metered by the selected model(s). - Tool calls and MCP sessions may be billed separately. - Streaming is settled after the stream ends via an async task. Example (non-streaming HTTP): POST /v1/chat/completions Content-Type: application/json Authorization: Bearer { "model": "provider/model-name", "messages": [{"role": "user", "content": "Hello"}] } 200 OK { "id": "cmpl_123", "object": "chat.completion", "choices": [ {"index": 0, "message": {"role": "assistant", "content": "Hi there!"}, "finish_reason": "stop"} ], "usage": {"prompt_tokens": 3, "completion_tokens": 4, "total_tokens": 7} } Example (streaming over SSE): POST /v1/chat/completions Accept: text/event-stream data: {"id":"cmpl_123","choices":[{"index":0,"delta":{"content":"Hi"}}]} data: {"id":"cmpl_123","choices":[{"index":0,"delta":{"content":" there!"}}]} data: [DONE] # Create Embeddings Source: https://docs.dedaluslabs.ai/api/v1/create-embeddings /openapi.json post /v1/embeddings Create embeddings using the configured provider. # Create Image Source: https://docs.dedaluslabs.ai/api/v1/create-image /openapi.json post /v1/images/generations Generate images from text prompts. Pure image generation models only (DALL-E, GPT Image). For multimodal models like gemini-2.5-flash-image, use /v1/chat/completions. # Create Speech Source: https://docs.dedaluslabs.ai/api/v1/create-speech /openapi.json post /v1/audio/speech Generate speech audio from text. Generates audio from the input text using text-to-speech models. Supports multiple voices and output formats including mp3, opus, aac, flac, wav, and pcm. Returns streaming audio data that can be saved to a file or streamed directly to users. # Create Transcription Source: https://docs.dedaluslabs.ai/api/v1/create-transcription /openapi.json post /v1/audio/transcriptions Transcribe audio into text. Transcribes audio files using OpenAI's Whisper model. Supports multiple audio formats including mp3, mp4, mpeg, mpga, m4a, wav, and webm. Maximum file size is 25 MB. Args: file: Audio file to transcribe (required) model: Model ID to use (e.g., "openai/whisper-1") language: ISO-639-1 language code (e.g., "en", "es") - improves accuracy prompt: Optional text to guide the model's style response_format: Format of the output (json, text, srt, verbose_json, vtt) temperature: Sampling temperature between 0 and 1 Returns: Transcription object with the transcribed text # Create Translation Source: https://docs.dedaluslabs.ai/api/v1/create-translation /openapi.json post /v1/audio/translations Translate audio into English. Translates audio files in any supported language to English text using OpenAI's Whisper model. Supports the same audio formats as transcription. Maximum file size is 25 MB. Args: file: Audio file to translate (required) model: Model ID to use (e.g., "openai/whisper-1") prompt: Optional text to guide the model's style response_format: Format of the output (json, text, srt, verbose_json, vtt) temperature: Sampling temperature between 0 and 1 Returns: Translation object with the English translation # Changelog Source: https://docs.dedaluslabs.ai/changelog/index Latest updates and releases for Dedalus Labs ### Dedalus API **Features** * Support for Opus 4.5 streaming * Support for Gemini model generation tool calling functions * Expanded support for Grok models * Expanded support for DeepSeek models ### Dedalus Cloud **Non-compliant Servers Deprecation Notice** Starting January 12, 2026, some MCP servers will be deprecated, and affected users will be notified by email. To comply with MCP latest protocol, servers must support authentication to securely store credentials. The servers that will be deprecated do not meet this requirement. See our [MCP server guide](/sdk/agents/guides/server-guidelines) for migration instructions. ## Structured Outputs and TypeScript SDK This release introduces structured outputs for the Python SDK and the launch of our TypeScript SDK. ### dedalus-labs-sdk (Python) **Features** * Structured Outputs: Added support for Pydantic-powered structured outputs. This includes a new `chat.completions.parse()` method to automatically deserialize the response content into a Pydantic model, ensuring strict adherence to JSON schema with OpenAI models. ### dedalus-labs-sdk (TypeScript) **Features** * TypeScript SDK Launch: The `dedalus-labs` and `dedalus-labs-mcp` packages are now available on npm, featuring type-safe JSON responses with Zod schemas. ## Streaming Structured Outputs ### dedalus-labs-sdk (Python) **Features** * Streaming support for structured outputs: Stream partial results while parsing into Pydantic models * Pydantic stream helper: New `stream_helper` for incremental structured data * Response format standardization across all providers **Compatibility** * Python 3.14 support * Improved Pydantic v1 compatibility for `model_dump` and `model_dump_json` signatures * Dropped Python 3.8 support (minimum version is now 3.9+) ## Structured Outputs for Tools ### dedalus-labs-sdk (Python) **Features** * Structured outputs for tool definitions: Define tool parameters using Pydantic models * Flexible `.parse()` input: Accept various input formats for the parse method * Nullable messages parameter for simpler API calls ## Image Support and Auto-Executing Tools ### dedalus-labs-sdk (Python) **Features** * Image editing and variation support via the images API * Vision format helper: Simplified image content formatting for multimodal models * Auto-executing tools: Tools can now be configured to execute automatically based on model responses * File upload support for multimodal requests ## Runner Improvements ### dedalus-labs-sdk (Python) **Features** * Conversation history access: Access the full conversation history from runner instances * Instructions parameter: Pass custom system instructions to runners at runtime * Pydantic v3 forward compatibility ## DedalusModel ### dedalus-labs-sdk (Python) **Features** * `DedalusModel` type: A unified model identifier that works across all supported providers * Model parameter extraction: Automatically extracts provider-specific parameters with warnings for unsupported options * Decoupled `Model` and `DedalusModel` types for cleaner API boundaries ## API Standardization ### dedalus-labs-sdk (Python) **Improvements** * Standardized parameter naming: `messages=` for completions, `input=` for runner ## Chat Completions and Schema Generation ### dedalus-labs-sdk (Python) **Features** * Chat completions API: Full support for the chat completions endpoint * `to_schema()` method: Generate JSON schemas from Pydantic models for structured outputs * `ModelConfig`: Configure model-specific parameters programmatically * Streaming support with configurable options ## Streaming Schemas ### dedalus-labs-sdk (Python) **Features** * Streaming response schemas: Type-safe streaming with proper schema definitions * File upload requests: Initial support for multipart file uploads ## SDK Publication First public release of the Dedalus SDK on package registries. ### dedalus-labs-sdk **Features** * Published `dedalus-labs` package on PyPI * Published `dedalus-labs` package on npm * Published `dedalus-labs-mcp` package on npm [//]: # "AUTO-SDK-UPDATES:START" * Repository: `dedalus-labs/dedalus-sdk-python` * Version: `v0.2.0` * Published: 2026-01-09 * Notes: [View release notes](https://github.com/dedalus-labs/dedalus-sdk-python/releases/tag/v0.2.0) * Repository: `dedalus-labs/dedalus-sdk-typescript` * Version: `v0.1.0-alpha.8` * Published: 2025-11-26 * Notes: [View release notes](https://github.com/dedalus-labs/dedalus-sdk-typescript/releases/tag/v0.1.0-alpha.8) * Repository: `dedalus-labs/dedalus-sdk-typescript` * Version: `v0.1.0-alpha.6` * Published: 2025-11-25 * Notes: [View release notes](https://github.com/dedalus-labs/dedalus-sdk-typescript/releases/tag/v0.1.0-alpha.6) * Repository: `dedalus-labs/dedalus-sdk-python` * Version: `v0.1.1` * Published: 2025-11-12 * Notes: [View release notes](https://github.com/dedalus-labs/dedalus-sdk-python/releases/tag/v0.1.1) * Repository: `dedalus-labs/dedalus-sdk-python` * Version: `v0.1.0` * Published: 2025-11-09 * Notes: [View release notes](https://github.com/dedalus-labs/dedalus-sdk-python/releases/tag/v0.1.0) * Repository: `dedalus-labs/dedalus-sdk-go` * Version: `v0.1.0-alpha.3` * Published: 2025-11-08 * Notes: [View release notes](https://github.com/dedalus-labs/dedalus-sdk-go/releases/tag/v0.1.0-alpha.3) * Repository: `dedalus-labs/dedalus-sdk-typescript` * Version: `v0.1.0-alpha.5` * Published: 2025-11-08 * Notes: [View release notes](https://github.com/dedalus-labs/dedalus-sdk-typescript/releases/tag/v0.1.0-alpha.5) * Repository: `dedalus-labs/dedalus-sdk-python` * Version: `v0.0.1` * Published: 2025-11-08 * Notes: [View release notes](https://github.com/dedalus-labs/dedalus-sdk-python/releases/tag/v0.0.1) * Repository: `dedalus-labs/dedalus-sdk-python` * Version: `v0.1.0-alpha.10` * Published: 2025-11-08 * Notes: [View release notes](https://github.com/dedalus-labs/dedalus-sdk-python/releases/tag/v0.1.0-alpha.10) * Repository: `dedalus-labs/dedalus-sdk-python` * Version: `v0.1.0-alpha.9` * Published: 2025-09-20 * Notes: [View release notes](https://github.com/dedalus-labs/dedalus-sdk-python/releases/tag/v0.1.0-alpha.9) * Repository: `dedalus-labs/dedalus-sdk-python` * Version: `v0.1.0-alpha.8` * Published: 2025-08-21 * Notes: [View release notes](https://github.com/dedalus-labs/dedalus-sdk-python/releases/tag/v0.1.0-alpha.8) * Repository: `dedalus-labs/dedalus-sdk-python` * Version: `v0.1.0-alpha.7` * Published: 2025-08-21 * Notes: [View release notes](https://github.com/dedalus-labs/dedalus-sdk-python/releases/tag/v0.1.0-alpha.7) * Repository: `dedalus-labs/dedalus-sdk-python` * Version: `v0.1.0-alpha.6` * Published: 2025-08-21 * Notes: [View release notes](https://github.com/dedalus-labs/dedalus-sdk-python/releases/tag/v0.1.0-alpha.6) * Repository: `dedalus-labs/dedalus-sdk-python` * Version: `v0.1.0-alpha.5` * Published: 2025-08-18 * Notes: [View release notes](https://github.com/dedalus-labs/dedalus-sdk-python/releases/tag/v0.1.0-alpha.5) * Repository: `dedalus-labs/dedalus-sdk-typescript` * Version: `v0.1.0-alpha.4` * Published: 2025-08-07 * Notes: [View release notes](https://github.com/dedalus-labs/dedalus-sdk-typescript/releases/tag/v0.1.0-alpha.4) * Repository: `dedalus-labs/dedalus-sdk-go` * Version: `v0.1.0-alpha.2` * Published: 2025-08-05 * Notes: [View release notes](https://github.com/dedalus-labs/dedalus-sdk-go/releases/tag/v0.1.0-alpha.2) * Repository: `dedalus-labs/dedalus-sdk-typescript` * Version: `v0.1.0-alpha.3` * Published: 2025-08-05 * Notes: [View release notes](https://github.com/dedalus-labs/dedalus-sdk-typescript/releases/tag/v0.1.0-alpha.3) * Repository: `dedalus-labs/dedalus-sdk-typescript` * Version: `v0.1.0-alpha.2` * Published: 2025-07-31 * Notes: [View release notes](https://github.com/dedalus-labs/dedalus-sdk-typescript/releases/tag/v0.1.0-alpha.2) * Repository: `dedalus-labs/dedalus-sdk-typescript` * Version: `v0.1.0-alpha.1` * Published: 2025-07-30 * Notes: [View release notes](https://github.com/dedalus-labs/dedalus-sdk-typescript/releases/tag/v0.1.0-alpha.1) * Repository: `dedalus-labs/dedalus-sdk-go` * Version: `v0.1.0-alpha.1` * Published: 2025-07-30 * Notes: [View release notes](https://github.com/dedalus-labs/dedalus-sdk-go/releases/tag/v0.1.0-alpha.1) [//]: # "AUTO-SDK-UPDATES:END" # Connect With Dedalus Source: https://docs.dedaluslabs.ai/community Build with the people pushing agent infrastructure forward. Get help, share what you're building, and keep up with launches, demos, and community events. ## Events RSVP to informal Dedalus gatherings and community meetups. Subscribe to upcoming workshops, demos, and community events. ## Merch Browse Dedalus merch drops and studio goods. Follow @archives.dedalus for merch previews and release notes. ## Resources Browse our open-source SDKs and report issues. Reach us directly for account or billing questions. ## Social Chat with the team and other developers. Follow us for product updates and announcements. Watch demos, launch videos, and product walkthroughs. Follow company news and hiring updates. See events, team posts, and community highlights. # Archives Source: https://docs.dedaluslabs.ai/community/archives Follow @archives.dedalus for merch previews and release notes. # Discord Source: https://docs.dedaluslabs.ai/community/discord Chat with the Dedalus team and other developers. # GitHub Source: https://docs.dedaluslabs.ai/community/github Browse Dedalus open-source SDKs and repositories. # Instagram Source: https://docs.dedaluslabs.ai/community/instagram See Dedalus events, team posts, and community highlights. # LinkedIn Source: https://docs.dedaluslabs.ai/community/linkedin Follow Dedalus company news and hiring updates. # Luma Source: https://docs.dedaluslabs.ai/community/luma Subscribe to upcoming Dedalus workshops, demos, and community events. # Partiful Source: https://docs.dedaluslabs.ai/community/partiful RSVP to informal Dedalus gatherings and community meetups. # Store Source: https://docs.dedaluslabs.ai/community/store Browse Dedalus merch drops and studio goods. # Support Source: https://docs.dedaluslabs.ai/community/support Email Dedalus support for account or billing questions. # X (Twitter) Source: https://docs.dedaluslabs.ai/community/x-twitter Follow Dedalus for product updates and announcements. # YouTube Source: https://docs.dedaluslabs.ai/community/youtube Watch Dedalus demos, launch videos, and product walkthroughs. # Agent Sandbox Per User Source: https://docs.dedaluslabs.ai/cookbook/agent-sandbox-per-user Give every chat user their own persistent Linux box. The agent's memory is the filesystem itself. For chat products that want their AI agent to actually *do work*: install packages, write files, run scripts. One Dedalus Machine per user, persistent home directory, sleeps between conversations. The user's `~/.bash_history`, project files, npm caches, and shell context all survive across sessions. The [browser agent cookbook](/cookbook/browser-agent) shows the agent loop. This page is the multi-tenant wrapper: provision lazily, sleep aggressively, route the right agent to the right machine. ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} npm install dedalus ``` ## 1. Provision lazily — one VM per user, on first message ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from "dedalus"; const dedalus = new Dedalus({ apiKey: process.env.DEDALUS_API_KEY! }); async function machineForUser(userId: string) { let id = await db.machineIdFor(userId); if (id) return id; const m = await dedalus.machines.create({ vcpu: 1, memory_mib: 2048, storage_gib: 10 }); await runAndWait(m.machine_id, ["/bin/bash", "-c", "apt-get update && apt-get install -y curl git python3-pip nodejs", ]); await db.setMachineIdFor(userId, m.machine_id); return m.machine_id; } ``` ## 2. On every chat turn: wake, drive, sleep ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} async function handleTurn(userId: string, userPrompt: string) { const id = await machineForUser(userId); await dedalus.machines.wake({ machine_id: id }); // openTerminal polls through wake_in_progress. See /cookbook/browser-agent // for the helper and the full WebSocket-driven agent loop. const term = await openTerminal(id); const reply = await runAgentLoop(term.stream_url, userPrompt); // Don't sleep eagerly here — auto-sleep handles idle. But you *can* // call sleep explicitly if you know the conversation just ended. return reply; } ``` That's the whole multi-tenant pattern. Per-user state is isolated by VM boundary; the agent has root inside its own box and zero access to anyone else's. ## Why a per-user VM beats a per-session container * **State persists across turns and sessions.** `cd /tmp/project && ls` followed three days later by `git status` Just Works. Containers reset; the agent re-installs Node every conversation. * **The filesystem is the agent's long-term memory.** Markdown notes the agent took, scripts it wrote, scraped data — all survive in `/root` indefinitely. No vector store required for "remember this project." * **Sleep-to-zero between conversations.** A user who chats for 10 minutes a day costs 10 minutes of compute, not 24 hours. * **Real `apt-get`, real Python venvs, real Cargo.** No Lambda layer hacks. ## Cost shape * \~\$X / vCPU-hour while awake (see [pricing](/dcs/dm/pricing)). * \~\$Y / GiB-month for sleeping storage. * A user who actively chats 30 min/day on a 1-vCPU machine pays for 0.5 hours of compute plus 10 GiB of storage per month. Sleeping users only pay storage. ## Security notes * **Per-user isolation is at the VM boundary.** Each machine runs its own Linux kernel. There's no shared-kernel container risk. * **Outbound egress is on by default.** If the agent is allowed to run untrusted code on the user's behalf, treat the VM as compromisable and don't store cross-tenant secrets there. Issue scoped credentials per machine. * **Inbound is closed by default.** A user's machine isn't reachable from the internet unless you explicitly create a [port](/dcs/dm/ports) for it. ## See also * [Browser Agent](/cookbook/browser-agent) — the agent loop driving the terminal. * [Terminals](/dcs/dm/terminals) — the WebSocket protocol your agent talks. * [Lifecycle](/dcs/dm/lifecycle) — sleep / wake / destroy semantics. # Browser Agent Source: https://docs.dedaluslabs.ai/cookbook/browser-agent An LLM agent that drives a real Linux shell on a Dedalus Machine over WebSocket. A terminal gives an agent the same primitives a human gets: a stateful shell, working directory, environment, real interactive programs. The agent writes keystrokes, reads bytes, decides the next command. ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} npm install dedalus ws @anthropic-ai/sdk ``` ```env .env theme={"theme":{"light":"github-light","dark":"github-dark"}} DEDALUS_API_KEY=your-dedalus-key ANTHROPIC_API_KEY=your-anthropic-key ``` ## 1. Create a machine and open a terminal ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from "dedalus"; import WebSocket from "ws"; const dedalus = new Dedalus({ apiKey: process.env.DEDALUS_API_KEY! }); async function openTerminal(machineId: string) { let terminal = await dedalus.machines.terminals.create({ machine_id: machineId, width: 100, height: 30, }); while (terminal.status === "wake_in_progress") { await new Promise((resolve) => setTimeout(resolve, terminal.retry_after_ms ?? 500), ); terminal = await dedalus.machines.terminals.retrieve({ machine_id: machineId, terminal_id: terminal.terminal_id, }); } if (terminal.status !== "ready" || !terminal.stream_url) { throw new Error(terminal.error_message ?? `terminal ${terminal.status}`); } return terminal; } const machine = await dedalus.machines.create({ vcpu: 2, memory_mib: 4096, storage_gib: 20, }); const term = await openTerminal(machine.machine_id); const ws = new WebSocket(term.stream_url); await new Promise((r) => ws.once("open", r)); ``` ## 2. Bridge stdin/stdout for the agent The agent writes a command, you send it as a Binary frame, you collect the bytes that come back until the shell prints a fresh prompt, then you hand the captured output to the agent. ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} function send(input: string) { ws.send(Buffer.from(input)); } function readUntilPrompt(promptRe = /\$\s$/): Promise { return new Promise((resolve) => { let buf = ""; const onMsg = (data: WebSocket.RawData) => { buf += data.toString("utf8"); if (promptRe.test(stripAnsi(buf))) { ws.off("message", onMsg); resolve(buf); } }; ws.on("message", onMsg); }); } const stripAnsi = (s: string) => s.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, ""); ``` ## 3. Loop the agent against the shell ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Anthropic from "@anthropic-ai/sdk"; const claude = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY! }); await readUntilPrompt(); // drain banner const history: { role: "user" | "assistant"; content: string }[] = [ { role: "user", content: "Find every Python file under ~ that mentions 'asyncio' and count the matching lines." }, ]; for (let step = 0; step < 8; step++) { const reply = await claude.messages.create({ model: "claude-opus-4-7", max_tokens: 512, system: "You drive a bash shell on a Linux VM. Reply with ONE shell command on a single line. " + "When done, reply with the literal word DONE.", messages: history, }); const cmd = (reply.content[0] as { text: string }).text.trim(); history.push({ role: "assistant", content: cmd }); if (cmd === "DONE") break; send(cmd + "\n"); const output = stripAnsi(await readUntilPrompt()); history.push({ role: "user", content: output }); } ws.close(); await dedalus.machines.delete({ machine_id: machine.machine_id }); ``` ## Notes * **Why a terminal and not [executions](/dcs/dm/executions)?** State persists across commands. `cd /tmp && ls` followed by `pwd` returns `/tmp`. Executions are independent and lose that. * **Resize the PTY** before running TUI programs (`top`, `htop`, `less`): send a Text frame `{"cols": 200, "rows": 50}`. * **Stripping ANSI** is only for the prompt-detection heuristic. If the agent itself benefits from colors and cursor codes (e.g., parsing `git status` output), pass the raw bytes through. * **Detect the prompt** robustly by setting `PS1` to a known sentinel (`export PS1='__DEDALUS_PROMPT__$ '`) at the start of the session. Regex against that instead of `\$\s$`. * **Timeouts**: wrap `readUntilPrompt` in a `Promise.race` against a timer; long-running commands (`apt-get install`) don't return a prompt for a while. # Cron on a Sleeping VM Source: https://docs.dedaluslabs.ai/cookbook/cron-on-sleeping-vm Replace Lambda cron with a real Linux box that wakes on schedule, runs the job, sleeps. A scheduled job that wakes a Dedalus Machine, runs your command, sleeps. You pay per-second of compute (only while the job runs) and storage stays warm between runs — your `~/.cache`, model weights, and installed packages persist. ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} npm install dedalus ``` ```env .env theme={"theme":{"light":"github-light","dark":"github-dark"}} DEDALUS_API_KEY=your-dedalus-key MACHINE_ID=dm-... ``` ## 1. Provision the machine once ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from "dedalus"; const dedalus = new Dedalus({ apiKey: process.env.DEDALUS_API_KEY! }); const m = await dedalus.machines.create({ vcpu: 1, memory_mib: 1024, storage_gib: 10 }); // One-time setup: install whatever your job needs, persists forever. await runAndWait(m.machine_id, ["/bin/bash", "-c", "apt-get update && apt-get install -y postgresql-client rclone && rclone config", ]); console.log("MACHINE_ID=" + m.machine_id); await dedalus.machines.sleep({ machine_id: m.machine_id }); ``` ## 2. The job runner ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} async function runAndWait(machineId: string, command: string[], timeoutMs = 600_000) { let execution = await dedalus.machines.executions.create({ machine_id: machineId, command, timeout_ms: timeoutMs, }); const terminalStatuses = new Set(["succeeded", "failed", "cancelled", "expired"]); while (!terminalStatuses.has(execution.status)) { await new Promise((r) => setTimeout(r, 500)); execution = await dedalus.machines.executions.retrieve({ machine_id: machineId, execution_id: execution.execution_id, }); } if (execution.status !== "succeeded") { throw new Error(execution.error_message ?? `execution ${execution.status}`); } return dedalus.machines.executions.output({ machine_id: machineId, execution_id: execution.execution_id, }); } async function runCron() { const id = process.env.MACHINE_ID!; await dedalus.machines.wake({ machine_id: id }); const out = await runAndWait(id, ["/bin/bash", "-c", "pg_dump $DATABASE_URL | gzip | rclone rcat backup:nightly/$(date -I).sql.gz", ], 300_000); console.log(out.stdout); await dedalus.machines.sleep({ machine_id: id }); } runCron().catch((e) => { console.error(e); process.exit(1); }); ``` Run this script from any cron source: GitHub Actions on a schedule, EventBridge, your own k8s `CronJob`, even another sleeping VM running cron. ## When this beats Lambda * **No 250 MB layer hacks.** Run anything `apt-get` installs. * **No 15-minute timeout.** Set `timeout_ms` to whatever the job needs. * **Stateful caches.** `~/.cache/huggingface` (40 GB of weights), `~/.cargo`, build artifacts — all persist between runs without an S3 round-trip. * **Per-second billing while awake**, zero while sleeping. A 4-second job costs 4 seconds. * **Real Linux.** systemd services, persistent `crontab -e` if you'd rather schedule inside the VM, full process tree. ## Notes * **`timeout_ms` on executions** kills the process if it overruns. Without it, executions inherit a generous platform default — set it explicitly for cron. * **Auto-sleep** is on by default (`autosleep: "5m"`). Even if your script forgets to call `sleep()`, the machine slips back to zero cost five minutes later. * **Schedule the wake-up itself outside the VM.** A sleeping VM can't wake itself; you need an external scheduler to call `wake` on the machine ID. # Ephemeral CI Runners Source: https://docs.dedaluslabs.ai/cookbook/ephemeral-ci-runners GitHub Actions runners that sleep between jobs with toolchain caches hot on disk. A pool of self-hosted GitHub Actions runners, each backed by a Dedalus Machine. The first job warms `~/.cargo`, `~/.rustup`, `~/.npm`, `~/.cache/sccache`. Every job after that wakes the same machine and reuses everything. You pay only for the seconds a runner is actually running a job. ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} npm install dedalus ``` ## 1. Provision a runner pool Run this once per runner you want in the pool. Each machine registers itself as an Actions runner on first boot. ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from "dedalus"; const dedalus = new Dedalus({ apiKey: process.env.DEDALUS_API_KEY! }); async function provisionRunner(label: string, registrationToken: string) { const m = await dedalus.machines.create({ vcpu: 4, memory_mib: 8192, storage_gib: 40 }); await runAndWait(m.machine_id, ["/bin/bash", "-c", ` set -e apt-get update && apt-get install -y curl git build-essential useradd -m runner && cd /home/runner curl -L https://github.com/actions/runner/releases/latest/download/actions-runner-linux-x64.tar.gz | tar xz sudo -u runner ./config.sh --unattended --replace \ --url https://github.com/your-org \ --token ${registrationToken} \ --labels dedalus,${label} cat >/etc/systemd/system/gh-runner.service < { const body = await readJson(req); if (body.action === "queued" && body.workflow_job.labels.includes("dedalus")) { const id = await pickFreeMachine(); await dedalus.machines.wake({ machine_id: id }); // runner auto-picks up the job } res.end(); }).listen(8080); ``` That's the entire control loop. The runner's `gh-runner.service` is on by default; once the VM wakes, systemd starts the runner process and it polls the GitHub queue. After the job finishes the runner goes idle. Five minutes later the platform auto-sleeps the VM (`autosleep`, default `"5m"`). ## Why this beats GitHub-hosted runners * **Hot toolchain caches.** `~/.cargo` (3 GB of crates), `~/.rustup`, `~/.cache/sccache`, `~/.npm`, `node_modules` for monorepo workspaces — all stay on disk between jobs. GitHub-hosted runners cold-pull every job. A Rust job that takes 12 minutes cold takes 90 seconds warm. * **Pay only for awake-seconds.** A pool of 10 runners that handle a 4-hour-per-day workload pays for \~40 vCPU-hours, not 240. * **4 vCPU / 8 GiB / 40 GiB** out of the box; provision bigger if you need it. Standard GitHub runners cap at 2 vCPU / 7 GiB. * **Real Linux**, your kernel — install kernel modules, run Docker-in-VM with full nesting, run privileged containers, whatever your build needs. ## Notes * **Pre-warming.** If you want zero cold-start on the first job of the day, run a dummy `wake` on cron a few minutes before peak. (See [Cron on a Sleeping VM](/cookbook/cron-on-sleeping-vm).) * **Pool size.** Provision N machines; a queued job wakes the first idle one. Concurrency above N queues — your webhook handler should fall back to creating a new machine if the pool is saturated. * **Caches** survive sleep and wake. They don't survive `delete`. If you provision a new machine you lose its cache; budget for that on first build. * **Auto-sleep timing.** Default 300s of idle is fine for back-to-back PRs. Push it higher (`autosleep: "30m"`) for spiky CI patterns. # OpenClaw on Dedalus Source: https://docs.dedaluslabs.ai/cookbook/openclaw Self-hosted AI assistant with a WebSocket gateway and OpenAI-compatible chat API, running inside a Dedalus Machine. [OpenClaw](https://openclaw.ai) on a Dedalus Machine. No SSH. Everything goes through the execution API. Full script: [dedalus-labs/openclaw-ddls](https://github.com/dedalus-labs/openclaw-ddls). ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} npm install dedalus dotenv ``` ```env .env theme={"theme":{"light":"github-light","dark":"github-dark"}} DEDALUS_API_KEY=your-dedalus-key ANTHROPIC_API_KEY=your-anthropic-key ``` ## 1. Create the machine ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import "dotenv/config"; import Dedalus from "dedalus"; const client = new Dedalus({ apiKey: process.env.DEDALUS_API_KEY }); const ws = await client.machines.create({ vcpu: 2, memory_mib: 4096, storage_gib: 10 }); let m = ws; while (m.status.phase !== "running") { await new Promise((s) => setTimeout(s, 2000)); m = await client.machines.retrieve({ machine_id: ws.machine_id }); } const mid = ws.machine_id; ``` `exec(mid, cmd)` below is a thin wrapper around `client.machines.executions.create` that polls until `succeeded`/`failed` and returns stdout. See the [reference repo](https://github.com/dedalus-labs/openclaw-ddls) for the 15-line helper. ## 2. Install OpenClaw into persistent storage Root fs is small and ephemeral. Redirect everything npm writes (prefix, cache, tmp) into `/home/machine/`. ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} await exec(mid, "curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && apt-get install -y nodejs"); await exec(mid, "mkdir -p /home/machine/{.npm-global,.npm-cache,.tmp,.openclaw} && " + "NPM_CONFIG_PREFIX=/home/machine/.npm-global " + "NPM_CONFIG_CACHE=/home/machine/.npm-cache " + "TMPDIR=/home/machine/.tmp " + "npm install -g openclaw@latest" ); ``` ## 3. Configure, then launch the gateway Enable the HTTP endpoint *before* starting the gateway or you restart it. `setsid` is how you detach: the execution API waits on foreground processes, and `nohup &` alone inherits the session. ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} const ENV = "export PATH=/home/machine/.npm-global/bin:$PATH HOME=/home/machine OPENCLAW_STATE_DIR=/home/machine/.openclaw"; await exec(mid, `${ENV} && openclaw config set gateway.mode local`); await exec(mid, `${ENV} && openclaw config set env.vars.ANTHROPIC_API_KEY "${process.env.ANTHROPIC_API_KEY}"`); await exec(mid, `${ENV} && openclaw config set gateway.http.endpoints.chatCompletions.enabled true`); await exec(mid, `pgrep -f openclaw-gateway > /dev/null || ` + `(setsid bash -c '${ENV} && exec openclaw gateway run --auth none > /home/machine/.openclaw/gateway.log 2>&1' /dev/null & disown; sleep 10)` ); ``` ## 4. Chat ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} const r = await exec(mid, `curl -sS http://127.0.0.1:18789/v1/chat/completions ` + `-H 'Content-Type: application/json' ` + `-d '{"model":"openclaw/default","messages":[{"role":"user","content":"Hello!"}]}'` ); console.log(JSON.parse(r).choices[0].message.content); ``` `model` is an agent target. Override the backing LLM with `x-openclaw-model: anthropic/claude-sonnet-4-6`. Streaming with `"stream": true`. Sessions share history by `user`. `openclaw.ts` end-to-end + `chat.ts` replayer. # PR Preview Environments Source: https://docs.dedaluslabs.ai/cookbook/pr-preview-environments A full-stack preview environment per pull request. Sleeps when idle, wakes on a reviewer click. One Dedalus Machine per open PR. The PR's branch is checked out, dependencies installed, services running. When nobody touches the preview URL for 5 minutes the VM sleeps. When the next reviewer opens the link, it wakes in under a second with the database, seed data, and log history intact. ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} npm install dedalus ``` ## 1. On PR opened: provision ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from "dedalus"; const dedalus = new Dedalus({ apiKey: process.env.DEDALUS_API_KEY! }); async function waitForPreview(machineId: string, previewId: string) { let preview = await dedalus.machines.previews.retrieve({ machine_id: machineId, preview_id: previewId, }); while (preview.status === "wake_in_progress") { await new Promise((resolve) => setTimeout(resolve, preview.retry_after_ms ?? 500), ); preview = await dedalus.machines.previews.retrieve({ machine_id: machineId, preview_id: previewId, }); } if (preview.status !== "ready" || !preview.url) { throw new Error(preview.error_message ?? `preview ${preview.status}`); } return preview; } async function provisionPreview(prNumber: number, branch: string) { const m = await dedalus.machines.create({ vcpu: 2, memory_mib: 4096, storage_gib: 20, }); await runAndWait(m.machine_id, ["/bin/bash", "-c", ` set -e apt-get update && apt-get install -y git nodejs postgresql git clone --depth 1 -b ${branch} https://github.com/your-org/your-app /root/app cd /root/app && npm install pg_ctlcluster 16 main start && createdb app && npm run migrate && npm run seed nohup npm run dev -- --host 0.0.0.0 --port 3000 >/var/log/app.log 2>&1 & `]); const preview = await dedalus.machines.previews.create({ machine_id: m.machine_id, port: 3000, protocol: "https", visibility: "org", // or "public" for external reviewers }); const ready = await waitForPreview(m.machine_id, preview.preview_id); return { machineId: m.machine_id, url: ready.url }; } ``` ## 2. On webhook hit (or middleware): wake The simplest pattern is a small router that proxies `https://pr-123.previews.your-app.com` to the machine's preview URL. Before forwarding, wake the machine. ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import http from "node:http"; async function waitUntilRunning(machineId: string) { let machine = await dedalus.machines.wake({ machine_id: machineId }); while (machine.status.phase !== "running") { if (machine.status.phase === "failed") { throw new Error(machine.status.reason); } await new Promise((resolve) => setTimeout(resolve, 500)); machine = await dedalus.machines.retrieve({ machine_id: machineId }); } } http.createServer(async (req, res) => { const prNumber = parseInt(req.headers["x-pr-number"] as string); const machineId = await lookupMachine(prNumber); // your DB await waitUntilRunning(machineId); // ...then proxy req → the preview URL stored at provision time. }).listen(8080); ``` `wake` is idempotent, but its response may still be `placement_pending` or `starting`. Queue the request until the machine reaches `running`, then forward it to the stored preview URL. ## 3. On PR merged or closed: destroy ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} async function teardownPreview(prNumber: number) { const machineId = await lookupMachine(prNumber); await dedalus.machines.delete({ machine_id: machineId }); } ``` ## Why a microVM beats a container here * **Postgres, Redis, real systemd**, anything else that doesn't survive container restart, just runs. Migrations + seed data persist across sleep/wake. * **Sleep means zero compute cost** while the PR is dormant. A 3-week-old PR costs only the storage GiB-month — not 24/7 uptime. * **Fast resume.** Queue the first request while the machine restores, then forward it normally. * **Idle auto-sleep is built in** (`autosleep`, default `"5m"`). If your wake-on-traffic proxy forgets to put the VM back to sleep, the platform does it for you after five idle minutes. ## Notes * **Set `visibility: "public"`** if external collaborators need to reach the preview without a Dedalus account. Use `"org"` to gate it on org membership, or `"private"` for the creator only. * **Wake latency** depends on storage size. A 20 GiB machine wakes faster than a 200 GiB one. * **Serialize cold-start traffic per machine.** Queue concurrent requests behind one readiness check instead of forwarding while the machine is still starting. # Connect to terminal WebSocket stream Source: https://docs.dedaluslabs.ai/dcs/api/machine-lifecycle/connect-to-terminal-websocket-stream /dcs-openapi.json get /v1/machines/{machine_id}/terminals/{terminal_id}/stream Upgrades to a WebSocket connection for interactive terminal I/O. Clients send JSON `TerminalClientEvent` messages and receive JSON `TerminalServerEvent` messages. Terminal byte streams are base64-encoded inside `input` and `output` events; `resize` events use integer `width` and `height` fields. # Create execution Source: https://docs.dedaluslabs.ai/dcs/api/machine-lifecycle/create-execution /dcs-openapi.json post /v1/machines/{machine_id}/executions # Create port Source: https://docs.dedaluslabs.ai/dcs/api/machine-lifecycle/create-port /dcs-openapi.json post /v1/machines/{machine_id}/ports # Create SSH session Source: https://docs.dedaluslabs.ai/dcs/api/machine-lifecycle/create-ssh-session /dcs-openapi.json post /v1/machines/{machine_id}/ssh # Create terminal Source: https://docs.dedaluslabs.ai/dcs/api/machine-lifecycle/create-terminal /dcs-openapi.json post /v1/machines/{machine_id}/terminals # Delete artifact Source: https://docs.dedaluslabs.ai/dcs/api/machine-lifecycle/delete-artifact /dcs-openapi.json delete /v1/machines/{machine_id}/artifacts/{artifact_id} # Delete execution Source: https://docs.dedaluslabs.ai/dcs/api/machine-lifecycle/delete-execution /dcs-openapi.json delete /v1/machines/{machine_id}/executions/{execution_id} # Delete port Source: https://docs.dedaluslabs.ai/dcs/api/machine-lifecycle/delete-port /dcs-openapi.json delete /v1/machines/{machine_id}/ports/{port_id} # Delete SSH session Source: https://docs.dedaluslabs.ai/dcs/api/machine-lifecycle/delete-ssh-session /dcs-openapi.json delete /v1/machines/{machine_id}/ssh/{session_id} # Delete terminal Source: https://docs.dedaluslabs.ai/dcs/api/machine-lifecycle/delete-terminal /dcs-openapi.json delete /v1/machines/{machine_id}/terminals/{terminal_id} # Destroy machine Source: https://docs.dedaluslabs.ai/dcs/api/machine-lifecycle/destroy-machine /dcs-openapi.json delete /v1/machines/{machine_id} # Get artifact Source: https://docs.dedaluslabs.ai/dcs/api/machine-lifecycle/get-artifact /dcs-openapi.json get /v1/machines/{machine_id}/artifacts/{artifact_id} # Get execution Source: https://docs.dedaluslabs.ai/dcs/api/machine-lifecycle/get-execution /dcs-openapi.json get /v1/machines/{machine_id}/executions/{execution_id} # Get execution output Source: https://docs.dedaluslabs.ai/dcs/api/machine-lifecycle/get-execution-output /dcs-openapi.json get /v1/machines/{machine_id}/executions/{execution_id}/output # Get port Source: https://docs.dedaluslabs.ai/dcs/api/machine-lifecycle/get-port /dcs-openapi.json get /v1/machines/{machine_id}/ports/{port_id} # Get SSH session Source: https://docs.dedaluslabs.ai/dcs/api/machine-lifecycle/get-ssh-session /dcs-openapi.json get /v1/machines/{machine_id}/ssh/{session_id} # Get terminal Source: https://docs.dedaluslabs.ai/dcs/api/machine-lifecycle/get-terminal /dcs-openapi.json get /v1/machines/{machine_id}/terminals/{terminal_id} # List artifacts Source: https://docs.dedaluslabs.ai/dcs/api/machine-lifecycle/list-artifacts /dcs-openapi.json get /v1/machines/{machine_id}/artifacts # List execution events Source: https://docs.dedaluslabs.ai/dcs/api/machine-lifecycle/list-execution-events /dcs-openapi.json get /v1/machines/{machine_id}/executions/{execution_id}/events # List executions Source: https://docs.dedaluslabs.ai/dcs/api/machine-lifecycle/list-executions /dcs-openapi.json get /v1/machines/{machine_id}/executions # List ports Source: https://docs.dedaluslabs.ai/dcs/api/machine-lifecycle/list-ports /dcs-openapi.json get /v1/machines/{machine_id}/ports # List SSH sessions Source: https://docs.dedaluslabs.ai/dcs/api/machine-lifecycle/list-ssh-sessions /dcs-openapi.json get /v1/machines/{machine_id}/ssh # List terminals Source: https://docs.dedaluslabs.ai/dcs/api/machine-lifecycle/list-terminals /dcs-openapi.json get /v1/machines/{machine_id}/terminals # Sleep a running machine Source: https://docs.dedaluslabs.ai/dcs/api/machine-lifecycle/sleep-a-running-machine /dcs-openapi.json post /v1/machines/{machine_id}/sleep # Wake a sleeping machine Source: https://docs.dedaluslabs.ai/dcs/api/machine-lifecycle/wake-a-sleeping-machine /dcs-openapi.json post /v1/machines/{machine_id}/wake # Watch machine lifecycle status Source: https://docs.dedaluslabs.ai/dcs/api/machine-lifecycle/watch-machine-lifecycle-status /dcs-openapi.json get /v1/machines/{machine_id}/status/stream Streams machine lifecycle updates over Server-Sent Events. Each `status` event contains a full `LifecycleResponse` payload. The stream closes after the machine reaches its current desired state. # Get usage summary Source: https://docs.dedaluslabs.ai/dcs/api/usage/get-usage-summary /dcs-openapi.json get /v1/usage # List machine compute usage breakdown Source: https://docs.dedaluslabs.ai/dcs/api/usage/list-machine-compute-usage-breakdown /dcs-openapi.json get /v1/usage/machines/compute # List machine storage usage breakdown Source: https://docs.dedaluslabs.ai/dcs/api/usage/list-machine-storage-usage-breakdown /dcs-openapi.json get /v1/usage/machines/storage # Bring Your Own Key (BYOK) Source: https://docs.dedaluslabs.ai/guides/byok Use your own API keys to call providers directly through Dedalus BYOK lets you send requests through Dedalus using your own provider API key. The request still flows through our unified API (routing, tool calling, streaming, format normalization), but the LLM call is billed to your account with the provider. ## When to use BYOK * You have negotiated pricing or credits with a provider. * You want to use a model tier or region not available on our shared keys. * Your compliance policy requires that API keys stay under your control. ## Quick start Pass three headers (or SDK options) alongside your normal Dedalus API key: | Header | SDK option | Description | | ------------------ | ---------------- | ----------------------------------------------------- | | `X-Provider` | `provider` | Provider name (`openai`, `anthropic`, `google`, etc.) | | `X-Provider-Key` | `provider_key` | Your API key for that provider | | `X-Provider-Model` | `provider_model` | Model identifier at the provider (optional) | Only `X-Provider-Key` is strictly required. If you omit `X-Provider`, it is inferred from the model name. If you omit `X-Provider-Model`, the model from the request body is used. ## Examples ### curl ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} curl https://api.dedaluslabs.ai/v1/chat/completions \ -H "Authorization: Bearer $DEDALUS_API_KEY" \ -H "X-Provider: openai" \ -H "X-Provider-Key: $OPENAI_API_KEY" \ -H "X-Provider-Model: gpt-4o" \ -H "Content-Type: application/json" \ -d '{ "model": "openai/gpt-4o", "messages": [{"role": "user", "content": "Hello"}] }' ``` ### Python SDK ```python theme={"theme":{"light":"github-light","dark":"github-dark"}} from dedalus_labs import AsyncDedalus client = AsyncDedalus( provider="openai", provider_key="sk-your-openai-key", provider_model="gpt-4o", ) response = await client.chat.completions.create( model="openai/gpt-4o", messages=[{"role": "user", "content": "Hello"}], ) ``` ### TypeScript SDK ```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}} import Dedalus from "dedalus-labs"; const client = new Dedalus({ provider: "openai", providerKey: "sk-your-openai-key", providerModel: "gpt-4o", }); const response = await client.chat.completions.create({ model: "openai/gpt-4o", messages: [{ role: "user", content: "Hello" }], }); ``` ### Environment variables You can also set BYOK options via environment variables instead of passing them in code: ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}} export DEDALUS_PROVIDER="anthropic" export DEDALUS_PROVIDER_KEY="sk-ant-your-key" export DEDALUS_PROVIDER_MODEL="claude-sonnet-4-5-20250929" ``` The SDK picks these up automatically. No code changes needed. ## Per-request overrides The SDK options set defaults for every request. You can also override per-request by setting the headers directly: ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}} response = await client.chat.completions.create( model="google/gemini-2.5-pro", messages=[{"role": "user", "content": "Hello"}], extra_headers={ "X-Provider": "google", "X-Provider-Key": "your-google-key", }, ) ``` ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}} const response = await client.chat.completions.create( { model: "google/gemini-2.5-pro", messages: [{ role: "user", content: "Hello" }], }, { headers: { "X-Provider": "google", "X-Provider-Key": "your-google-key", }, }, ); ``` ## Supported providers Any provider in our [model list](/sdk/agents/guides/providers) works with BYOK: openai anthropic google xai mistral deepseek groq cohere perplexity cerebras together\_ai fireworks\_ai moonshot ## How it works Your request still goes through Dedalus. We handle routing, format normalization, streaming, and tool calling. The only difference is which API key is used for the upstream LLM call. ``` You → Dedalus API (your Dedalus key) → Provider (your provider key) → Response → You ``` BYOK keys are sent over HTTPS and are never stored. They are used for the duration of the request and discarded. If you need Dedalus to manage keys on your behalf, contact us at [support@dedaluslabs.ai](mailto:support@dedaluslabs.ai). ## Error handling | Scenario | What happens | | ------------------------------- | -------------------------------------------------- | | Invalid provider name | HTTP 400 with supported provider list | | Missing or invalid provider key | Provider returns its own auth error (usually 401) | | Model not available on provider | Provider returns its own model error (usually 404) | The error response always includes the upstream provider's error message so you can debug directly.