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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions agent-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@
"properties": {
"provider": {
"type": "string",
"description": "The underlying provider type. Defaults to \"openai\" when not set. Supported values: openai, anthropic, google, amazon-bedrock, dmr, and any built-in alias (requesty, openrouter, azure, xai, ollama, mistral, baseten, ovhcloud, groq, fireworks, deepseek, cerebras, together, huggingface, moonshot, vercel, cloudflare-workers-ai, cloudflare-ai-gateway, nvidia, etc.).",
"description": "The underlying provider type. Defaults to \"openai\" when not set. Supported values: openai, anthropic, google, amazon-bedrock, dmr, and any built-in alias (requesty, openrouter, azure, xai, ollama, mistral, baseten, ovhcloud, groq, fireworks, deepseek, cerebras, together, huggingface, moonshot, vercel, cloudflare-workers-ai, cloudflare-ai-gateway, nvidia, github-copilot, chatgpt, etc.).",
"examples": [
"openai",
"anthropic",
Expand Down Expand Up @@ -1446,7 +1446,8 @@
"anthropic",
"dmr",
"ollama",
"github-copilot"
"github-copilot",
"chatgpt"
]
},
"model": {
Expand Down
15 changes: 13 additions & 2 deletions cmd/root/doctor.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/docker/cli/cli"
"github.com/spf13/cobra"

"github.com/docker/docker-agent/pkg/chatgpt"
"github.com/docker/docker-agent/pkg/config"
"github.com/docker/docker-agent/pkg/environment"
"github.com/docker/docker-agent/pkg/model/provider/dmr"
Expand Down Expand Up @@ -266,8 +267,8 @@ func (f *doctorFlags) buildReport(ctx context.Context, agentRef string) (*doctor
if found, known := credFound[auto.Provider]; known && !found {
autoStatus.Usable = false
report.Issues = append(report.Issues, fmt.Sprintf(
"the configured default model %s/%s has no credential for provider %s; set %s (%s)",
auto.Provider, auto.Model, auto.Provider, primaryEnvVar[auto.Provider], environment.SecretsDocsURL))
"the configured default model %s/%s has no credential for provider %s; %s (%s)",
auto.Provider, auto.Model, auto.Provider, providerCredentialHint(auto.Provider, primaryEnvVar[auto.Provider]), environment.SecretsDocsURL))
}
}
report.AutoModel = autoStatus
Expand Down Expand Up @@ -366,6 +367,16 @@ func (f *doctorFlags) listDMRModels(ctx context.Context) ([]string, error) {
return dmr.ListModels(ctx)
}

// providerCredentialHint phrases the remediation for a missing provider
// credential: account-based providers point at the setup wizard's sign-in,
// the rest at their API-key env var.
func providerCredentialHint(provider, envVar string) string {
if provider == chatgpt.ProviderName {
return "sign in with `docker agent setup` (pick chatgpt) or set " + envVar
}
return "set " + envVar
}

// findSource returns the name of the first secret source that supplies a
// non-empty value for the variable. Empty values are skipped so a source that
// merely defines the variable (e.g. an env file with `KEY=`) is not reported
Expand Down
38 changes: 38 additions & 0 deletions cmd/root/doctor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,44 @@ func TestDoctorCommand_SourcePrecedenceMatchesProviderChain(t *testing.T) {
assert.Regexp(t, `openai\s+found\s+OPENAI_API_KEY\s+env-file`, output)
}

func TestDoctorCommand_ChatGPTLoginSource(t *testing.T) {
t.Parallel()

// The chatgpt-login source serves the virtual CHATGPT_OAUTH_TOKEN
// variable from the stored browser sign-in.
login := environment.NewMapEnvProvider(map[string]string{"CHATGPT_OAUTH_TOKEN": "chatgpt-access-token"})

output, err := executeDoctor(t, nil, func(f *doctorFlags) {
f.runConfig.EnvProviderForTests = login
f.sourcesForTests = []environment.Source{
{Name: "environment", Provider: environment.NewMapEnvProvider(nil)},
{Name: "chatgpt-login", Provider: login},
}
f.dmrLister = func(context.Context) ([]string, error) { return []string{"ai/qwen3:latest"}, nil }
f.loadUserConfig = func() (*userconfig.Config, error) { return &userconfig.Config{}, nil }
})

require.NoError(t, err)
assert.Regexp(t, `chatgpt\s+found\s+CHATGPT_OAUTH_TOKEN\s+chatgpt-login`, output)
assert.Contains(t, output, "auto -> chatgpt/gpt-5.2")
assert.Contains(t, output, "No issues found.")
assert.NotContains(t, output, "chatgpt-access-token", "secret values must never be printed")
}

func TestDoctorCommand_ChatGPTDefaultModelWithoutLoginSuggestsSignIn(t *testing.T) {
t.Parallel()

output, err := executeDoctor(t, nil,
withDoctorTestEnv(nil, []string{"ai/qwen3:latest"}, nil),
func(f *doctorFlags) {
f.runConfig.DefaultModel = &latest.ModelConfig{Provider: "chatgpt", Model: "gpt-5.2"}
})

require.Error(t, err, "a default model without credentials is an issue")
assert.Regexp(t, `chatgpt\s+not set\s+CHATGPT_OAUTH_TOKEN`, output)
assert.Contains(t, output, "sign in with `docker agent setup` (pick chatgpt) or set CHATGPT_OAUTH_TOKEN")
}

func TestDoctorCommand_EmptyValueIsNotACredential(t *testing.T) {
t.Parallel()

Expand Down
43 changes: 34 additions & 9 deletions cmd/root/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/spf13/cobra"
"golang.org/x/term"

"github.com/docker/docker-agent/pkg/chatgpt"
"github.com/docker/docker-agent/pkg/cli"
"github.com/docker/docker-agent/pkg/config"
"github.com/docker/docker-agent/pkg/environment"
Expand Down Expand Up @@ -52,10 +53,11 @@ type setupWizard struct {
in *bufio.Reader
out io.Writer

readSecret func(prompt string) (string, error)
stores []environment.SecretStore
dmrLister config.DMRModelLister
pullModel func(ctx context.Context, model string) error
readSecret func(prompt string) (string, error)
stores []environment.SecretStore
dmrLister config.DMRModelLister
pullModel func(ctx context.Context, model string) error
chatgptLogin func(ctx context.Context, out io.Writer) (*chatgpt.LoginResult, error)
}

func newSetupCmd() *cobra.Command {
Expand All @@ -66,7 +68,9 @@ func newSetupCmd() *cobra.Command {

Two paths:
- Cloud provider: pick a provider, paste its API key, and choose where to
store it (OS keychain, pass, or the docker agent env file).
store it (OS keychain, pass, or the docker agent env file). Picking
chatgpt signs in with your ChatGPT account in the browser instead of
asking for an API key.
- Local model: check Docker Model Runner and pull a model. No API key needed.

Ends with the exact command to start chatting. Secret values are stored where
Expand Down Expand Up @@ -116,9 +120,10 @@ func newTerminalSetupWizard(in io.Reader, out io.Writer) *setupWizard {
}
return string(value), nil
},
stores: environment.SecretStores(),
dmrLister: dmr.ListModels,
pullModel: dmr.Pull,
stores: environment.SecretStores(),
dmrLister: dmr.ListModels,
pullModel: dmr.Pull,
chatgptLogin: chatgpt.Login,
}
}

Expand Down Expand Up @@ -158,7 +163,11 @@ func (w *setupWizard) setupCloudProvider(ctx context.Context) (*setupResult, err
fmt.Fprintln(w.out)
fmt.Fprintln(w.out, "Pick a provider:")
for i, p := range providers {
fmt.Fprintf(w.out, " %2d. %-15s (%s)\n", i+1, p.Provider, p.EnvVars[0])
credential := p.EnvVars[0]
if p.Provider == chatgpt.ProviderName {
credential = "ChatGPT account sign-in, no API key"
}
fmt.Fprintf(w.out, " %2d. %-15s (%s)\n", i+1, p.Provider, credential)
}

choice, err := w.promptChoice(ctx, len(providers), 1)
Expand All @@ -168,6 +177,22 @@ func (w *setupWizard) setupCloudProvider(ctx context.Context) (*setupResult, err
selected := providers[choice-1]
envVar := selected.EnvVars[0]

// The chatgpt provider signs in with a browser flow instead of a pasted
// API key; the credential is stored by the login itself.
if selected.Provider == chatgpt.ProviderName {
fmt.Fprintln(w.out)
result, err := w.chatgptLogin(ctx, w.out)
if err != nil {
return nil, err
}
if result.Email != "" {
fmt.Fprintf(w.out, "Signed in as %s.\n", result.Email)
} else {
fmt.Fprintln(w.out, "Signed in with your ChatGPT account.")
}
return &setupResult{Model: selected.Provider + "/" + config.DefaultModels[selected.Provider]}, nil
}

key, err := w.promptSecret(ctx, fmt.Sprintf("\nPaste your %s API key (%s, input hidden): ", selected.Provider, envVar))
if err != nil {
return nil, err
Expand Down
33 changes: 33 additions & 0 deletions cmd/root/setup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ import (
"context"
"errors"
"fmt"
"io"
"slices"
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/docker/docker-agent/pkg/chatgpt"
"github.com/docker/docker-agent/pkg/config"
"github.com/docker/docker-agent/pkg/config/latest"
"github.com/docker/docker-agent/pkg/environment"
Expand Down Expand Up @@ -135,6 +138,36 @@ func TestSetupWizard_CloudPathReasksOnEmptyKey(t *testing.T) {
assert.Equal(t, "sk-key", store.stored["ANTHROPIC_API_KEY"])
}

func TestSetupWizard_ChatGPTPathRunsBrowserSignIn(t *testing.T) {
t.Parallel()

providers := config.CloudProviderEnvVars()
idx := slices.IndexFunc(providers, func(p config.ProviderEnvVars) bool { return p.Provider == "chatgpt" })
require.GreaterOrEqual(t, idx, 0, "chatgpt must be offered by the wizard")

store := &fakeSecretStore{name: "keychain"}
// cloud -> chatgpt: no key prompt, no store prompt.
wizard, out, _ := newTestWizard(fmt.Sprintf("1\n%d\n", idx+1), nil, []environment.SecretStore{store}, nil, nil)
loginCalled := false
wizard.chatgptLogin = func(_ context.Context, _ io.Writer) (*chatgpt.LoginResult, error) {
loginCalled = true
return &chatgpt.LoginResult{Email: "user@example.com", Plan: "plus"}, nil
}

result, err := wizard.run(t.Context())
require.NoError(t, err)

assert.True(t, loginCalled)
assert.Empty(t, result.EnvVar, "the sign-in stores the credential itself; nothing to export")
assert.Equal(t, "chatgpt/"+config.DefaultModels["chatgpt"], result.Model)
assert.Empty(t, store.stored, "no secret store is involved")

output := out.String()
assert.Contains(t, output, "ChatGPT account sign-in")
assert.Contains(t, output, "Signed in as user@example.com.")
assert.Contains(t, output, "--model chatgpt/"+config.DefaultModels["chatgpt"])
}

func TestSetupWizard_LocalPathWithPulledModels(t *testing.T) {
t.Parallel()

Expand Down
1 change: 1 addition & 0 deletions docs/concepts/models/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ for details.
| Azure OpenAI | `azure` | gpt-4o, gpt-5 on Azure | `AZURE_API_KEY` + `base_url` |
| Ollama | `ollama` | Any local Ollama model | None (local; optional `base_url`) |
| GitHub Copilot | `github-copilot` | Copilot-hosted OpenAI/Anthropic | `GITHUB_TOKEN` (PAT with `copilot`) |
| ChatGPT (OpenAI account) | `chatgpt` | gpt-5 family via ChatGPT subscription | None (sign in via `docker agent setup`) |

See the [Model Providers](../../providers/overview/index.md) section for detailed configuration guides.

Expand Down
2 changes: 1 addition & 1 deletion docs/configuration/models/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ models:
| Property | Type | Required | Description |
| --------------------- | ---------- | -------- | ------------------------------------------------------------------------------------- |
| `first_available` | array | ✗ | Candidate model references tried in order; selects the first whose credentials are configured. Mutually exclusive with other model settings. |
| `provider` | string | ✓/✗ | Required for regular model definitions; omitted for `first_available` selectors. Provider: `openai`, `anthropic`, `google`, `amazon-bedrock`, `dmr`, `mistral`, `xai`, `nebius`, `nvidia`, `minimax`, `baseten`, `ovhcloud`, `groq`, `fireworks`, `deepseek`, `cerebras`, `together`, `huggingface`, `moonshot`, `vercel`, `cloudflare-workers-ai`, `cloudflare-ai-gateway`, `requesty`, `openrouter`, `azure`, `ollama`, `github-copilot`, or any [named provider](../../providers/custom/index.md). |
| `provider` | string | ✓/✗ | Required for regular model definitions; omitted for `first_available` selectors. Provider: `openai`, `anthropic`, `google`, `amazon-bedrock`, `dmr`, `mistral`, `xai`, `nebius`, `nvidia`, `minimax`, `baseten`, `ovhcloud`, `groq`, `fireworks`, `deepseek`, `cerebras`, `together`, `huggingface`, `moonshot`, `vercel`, `cloudflare-workers-ai`, `cloudflare-ai-gateway`, `requesty`, `openrouter`, `azure`, `ollama`, `github-copilot`, `chatgpt`, or any [named provider](../../providers/custom/index.md). |
| `model` | string | ✓/✗ | Required for regular model definitions; omitted for `first_available` selectors. Model name (e.g., `gpt-4o`, `claude-sonnet-4-5`, `gemini-3.5-flash`) |
| `temperature` | float | ✗ | Sampling randomness. Range is provider-dependent — typically `0.0–2.0` (Anthropic caps at `1.0`). `0.0` is deterministic. |
| `max_tokens` | int | ✗ | Maximum response length in tokens |
Expand Down
122 changes: 122 additions & 0 deletions docs/providers/chatgpt/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
---
title: "ChatGPT (OpenAI account)"
description: "Use your ChatGPT Plus/Pro/Business subscription with docker-agent by signing in with your OpenAI account, no API key needed."
keywords: docker agent, ai agents, model providers, llm, chatgpt, openai, codex, subscription
weight: 55
canonical: https://docs.docker.com/ai/docker-agent/providers/chatgpt/
---

_Use your ChatGPT subscription with docker-agent by signing in with your OpenAI account. No API key needed._

## Overview

The `chatgpt` provider authenticates with a ChatGPT account (the same
"Sign in with ChatGPT" flow used by OpenAI's Codex CLI) instead of an
`OPENAI_API_KEY`. Usage is billed against your ChatGPT Plus, Pro, or
Business plan rather than pay-per-token API credits.

Under the hood, docker-agent talks to the ChatGPT Codex backend
(`https://chatgpt.com/backend-api/codex`), which serves the `gpt-5` model
family over the OpenAI Responses API.

## Prerequisites

- A paid **ChatGPT** subscription (Plus, Pro, or Business).
- A browser on the machine running the sign-in (the OAuth flow uses a
fixed `localhost:1455` callback).

## Sign In

```bash
docker agent setup
```

Pick **chatgpt** in the provider list: instead of asking for an API key, the
wizard opens your browser on the ChatGPT sign-in page and stores the
resulting OAuth credential in the docker-agent config directory
(`~/.config/cagent/chatgpt-auth.json`, owner-only permissions). The access
token is refreshed automatically; you only need to sign in again if the
refresh token is revoked.

Related commands:

```bash
docker agent doctor # the chatgpt row shows the credential state
rm ~/.config/cagent/chatgpt-auth.json # sign out (remove the stored sign-in)
```

## Configuration

### Inline

```yaml
agents:
root:
model: chatgpt/gpt-5.2
instruction: You are a helpful assistant.
```

### Named model

```yaml
models:
gpt:
provider: chatgpt
model: gpt-5.2
thinking_budget: medium

agents:
root:
model: gpt
```

## Available Models

The Codex backend serves the models available to your ChatGPT plan,
typically:

| Model | Best For |
| ------------------- | ------------------------------------- |
| `gpt-5.2` | General purpose, strong reasoning |
| `gpt-5.2-codex` | Agentic coding workflows |
| `gpt-5.1` | Previous flagship |
| `gpt-5.1-codex-mini`| Fast and cheap coding tasks |

## How It Works

- **Auth:** the `docker agent setup` sign-in runs an OAuth 2.0
authorization-code + PKCE flow against `auth.openai.com`. The stored login
is exposed to credential checks (doctor, `first_available`, auto model
selection) as the virtual `CHATGPT_OAUTH_TOKEN` variable.
- **API:** requests go to the Responses API only; the backend has no Chat
Completions endpoint, so `api_type` is pinned automatically.
- **Request shape:** the backend requires stateless requests (`store: false`)
and a top-level `instructions` field, so docker-agent moves system messages
there. Client-side sampling parameters (`temperature`, `top_p`,
`max_tokens`) are not supported by the backend and are dropped.

## Setting the Token Explicitly

`CHATGPT_OAUTH_TOKEN` can also be set like any other credential (shell
environment, `--env-from-file`, keychain, ...). An explicitly set value takes
precedence over the stored sign-in. This is useful for short-lived CI runs
with a pre-minted access token, but note that such a token expires and is not
refreshed.

## ChatGPT Subscription vs. OpenAI API Key

| | `chatgpt` | `openai` |
| --- | --- | --- |
| Credential | ChatGPT account sign-in | `OPENAI_API_KEY` |
| Billing | Included in the ChatGPT plan (rate-limited) | Pay per token |
| Models | `gpt-5` family served by the Codex backend | Full OpenAI API catalog |
| Sampling controls (`temperature`, ...) | Not supported | Supported |
| Embeddings / reranking | Not supported | Supported |

When both credentials are configured, automatic model selection prefers
`openai`; pin `--model chatgpt/gpt-5.2` (or use a named model) to use the
subscription.

> [!NOTE]
> Use of the Codex backend is governed by OpenAI's terms for ChatGPT and
> Codex. Sign-in is per user; do not share the stored credential.
5 changes: 5 additions & 0 deletions docs/providers/openai/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ _Use GPT-4o, GPT-5, GPT-5-mini, and other OpenAI models with docker-agent._
export OPENAI_API_KEY="sk-..."
```

> [!TIP]
> No API key? A ChatGPT Plus/Pro/Business subscription can be used instead
> through the [`chatgpt` provider](../chatgpt/index.md): sign in once with
> `docker agent setup` (pick chatgpt).

## Configuration

### Inline
Expand Down
1 change: 1 addition & 0 deletions docs/providers/overview/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ docker-agent also includes built-in aliases for these providers:

| Provider | Alias | API Key / Env Variable |
| -------------- | ---------------- | ----------------------------------- |
| ChatGPT (OpenAI account) | [`chatgpt`](../chatgpt/index.md) | None (sign in via `docker agent setup`) |
| OpenCode Zen | `opencode-zen` | `OPENCODE_API_KEY` |
| OpenCode Go | `opencode-go` | `OPENCODE_API_KEY` |
| Mistral | `mistral` | `MISTRAL_API_KEY` |
Expand Down
Loading
Loading