Skip to content
On this page

Sophy documentation

Build on a governed AI gateway

Use supported OpenAI client methods at https://sophy.in/v1. Each Sophy key is bound to one primary model and carries its policy, limits, and optional knowledge server-side.

Introduction

Sophy is an OpenAI-compatible gateway and operator console. Applications send input in familiar wire formats; Sophy authenticates the key, resolves its centrally managed configuration, calls the bound model with the project-owned credential for that model’s provider — the project’s Vercel AI Gateway credential, or its Cloudflare Workers AI credential for Cloudflare models — and records project-attributed usage.

Compatibility is intentionally scoped. Chat Completions and Responses cover text, streaming, multimodal input, and function-tool loops. Dedicated routes cover audio transcription, embeddings, image generation, short-lived file uploads, and the primary model bound to a key. Evaluation and extraction models are reached through native Sophy routes rather than an OpenAI client method. The sections below call out differences from the full OpenAI platform.

BASE URLhttps://sophy.in/v1

Supported API surfaces

Supported Sophy API routes
MethodRouteRequired capabilityCompatibility boundary
POST/v1/chat/completionsLanguage generationText, streaming, multimodal input, function tools
POST/v1/responsesLanguage generationStateless; full input required on every call
POST/v1/audio/transcriptionsTranscriptionMultipart audio; JSON with raw or policy-processed text
POST/v1/embeddingsEmbeddingString input; float or base64 vectors
POST/v1/evaluateEvaluationNative route; no OpenAI client method reaches it
POST/v1/extractExtractionNative route (preview); no OpenAI client method reaches it
POST/v1/images/generationsImage generationBase64 image output only
POST/v1/filesAny201 response; 4 MiB; cleanup-eligible after 24 hours
GET/v1/modelsAnyReturns only the primary model bound to this key

One key, one primary model

Bind keys around workloads and call a route the primary model’s capabilities support. The client-sent model never switches that model. A transcription key may separately name a language model for key-owned transcript processing.

Authentication

Send the Sophy key as a Bearer token. Keys look like mw_live_… and are issued inside a project in the Sophy console. The console and CLI use email codes for operator sign-in. A verified email can create a renameable My Project or accept an Admin invitation.

Authorization: Bearer mw_live_…

Missing Bearer authentication returns 401 missing_api_key. A malformed, unknown, revoked, expired, or inactive-project key returns 401 invalid_api_key. Keys are checked against the database on every request, so edits and revocation apply immediately.

Sophy keys and provider credentials

A Sophy API key authenticates your application to Sophy. A Project Admin separately connects the provider credentials that pay for and route the project’s AI calls: a Vercel AI Gateway key, which every project needs before it can create Sophy keys, and an optional Cloudflare Workers AI credential for Cloudflare’s Clef and Clairic models. Sophy never returns a stored provider secret.

To connect Cloudflare Workers AI, a Project Admin enters the project’s 32-character Cloudflare account ID and an API token created from Cloudflare’s “Workers AI” token template in project Settings, or runs sophy cloudflare connect (Sophy CLI 0.3.0 or later). Sophy checks the pair with one account-scoped Cloudflare call that runs no model, stores the token encrypted, and shows only its last four characters. Binding a key to a Cloudflare model requires that credential to be healthy, and those keys use only it — never the Vercel key, and never a Sophy-owned Cloudflare account.

Quickstart

Change the base URL and API key, then call one of the supported methods. The placeholder model is required by some OpenAI SDK methods but Sophy uses the model bound to the key.

from openai import OpenAI

client = OpenAI(
    base_url="https://sophy.in/v1",
    api_key="mw_live_…",
)

resp = client.chat.completions.create(
    model="sophy",  # ignored — the key owns the model
    messages=[{"role": "user", "content": "Hello, Sophy!"}],
)
print(resp.choices[0].message.content)

Chat Completions

POSThttps://sophy.in/v1/chat/completions
Chat Completions request fields
FieldTypeNotes
messagesarrayRequired and non-empty. Supports user, assistant, and tool turns plus text, image, and file content. System/developer handling depends on Agent mode.
streambooleanOptional. Emits OpenAI chat.completion.chunk SSE frames.
stream_optionsobjectOptional. { include_usage: true } adds a final usage-only chunk.
toolsarrayOptional function definitions. Sophy returns calls but never executes them. See Tool calling.
tool_choicestring | objectauto, none, required, or one named function.

Key-owned or ignored: model, temperature, top_p, max_tokens, max_completion_tokens, response_format, n, and user. Sophy applies the key’s configured model and generation parameters instead.

Buffered response

{
  "id": "chatcmpl-…",
  "object": "chat.completion",
  "created": 1735689600,
  "model": "anthropic/claude-sonnet-4.6",
  "choices": [{
    "index": 0,
    "message": { "role": "assistant", "content": "Hello! How can I help?" },
    "finish_reason": "stop"
  }],
  "usage": {
    "prompt_tokens": 9,
    "completion_tokens": 7,
    "total_tokens": 16,
    "prompt_tokens_details": { "cached_tokens": 0 }
  }
}

Streaming

A stream begins with an assistant-role chunk, emits text and/or tool-call deltas, finishes with a reason chunk, optionally emits usage, and ends with data: [DONE].

data: {"id":"chatcmpl-…","object":"chat.completion.chunk","created":1735689600,"model":"anthropic/claude-sonnet-4.6","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]}

data: {"id":"chatcmpl-…","object":"chat.completion.chunk","created":1735689600,"model":"anthropic/claude-sonnet-4.6","choices":[{"index":0,"delta":{"content":"Hello"},"finish_reason":null}]}

data: {"id":"chatcmpl-…","object":"chat.completion.chunk","created":1735689600,"model":"anthropic/claude-sonnet-4.6","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}

data: [DONE]

Responses API

POSThttps://sophy.in/v1/responses

Use client.responses.create(...) for a Responses-compatible text, multimodal, streaming, or function-tool workflow. Sophy is stateless: send the full conversation and any prior function calls/results in input on every request.

Responses API request fields
FieldTypeNotes
inputstring | arrayRequired. Must yield at least one usable message or tool item; send the full state for this call.
streambooleanOptional. Emits numbered, typed Responses SSE events.
toolsarrayOptional flat function-tool definitions. Non-function Responses tool types are ignored.
tool_choicestring | objectOptional. auto, none, required, or one named function.
instructionsstringUsed only when the key has Agent mode enabled; otherwise dropped.
previous_response_idstringNot supported. Returns 400 stateful_unsupported.
storebooleanIgnored. Sophy does not provide OpenAI-hosted response state and returns store: false.

Key-owned or ignored: model, temperature, top_p, max_output_tokens, and text.format.

from openai import OpenAI

client = OpenAI(base_url="https://sophy.in/v1", api_key="mw_live_…")

resp = client.responses.create(
    model="sophy",
    input="Write a haiku about gateways.",
)
print(resp.output_text)
{
  "id": "resp_…",
  "object": "response",
  "created_at": 1735689600,
  "status": "completed",
  "model": "anthropic/claude-sonnet-4.6",
  "store": false,
  "output": [{
    "id": "msg_…",
    "type": "message",
    "status": "completed",
    "role": "assistant",
    "content": [{ "type": "output_text", "text": "Silent routes converge…", "annotations": [] }]
  }],
  "usage": {
    "input_tokens": 11,
    "input_tokens_details": { "cached_tokens": 0 },
    "output_tokens": 9,
    "output_tokens_details": { "reasoning_tokens": 0 },
    "total_tokens": 20
  }
}

Streaming uses events such as response.created, response.output_text.delta, response.function_call_arguments.delta, and response.completed, each with an increasing sequence_number. There is no [DONE] sentinel.

Multimodal input

Language keys can send public image/file URLs or URLs returned by POST /v1/files, provided the bound model supports the content type. Sophy ownership-checks its own uploads; public external URLs pass through to the model provider.

uploaded_url = "https://…/uploads/<project>/<key>/invoice-….pdf"
uploaded_filename = "invoice.pdf"

resp = client.chat.completions.create(
    model="sophy",
    messages=[{
        "role": "user",
        "content": [
            {"type": "text", "text": "Summarize this document."},
            {
                "type": "file",
                "file": {
                    "file_url": uploaded_url,
                    "filename": uploaded_filename,
                },
            },
        ],
    }],
)

Model support still matters

The model bound to the key must accept the media you send. The model catalog in the console exposes capabilities such as image analysis and file input.

Image-input retention

When content logging is enabled, Sophy retains the exact image value supplied to Chat or Responses for seven days, then replaces it with a placeholder in the 30-day message log. An inline data URL contains the complete submitted bytes; for a public external URL, Sophy retains the URL rather than copying content owned by the external host. Turning content logging off retains neither form.

Tool / function calling

Sophy passes function definitions to the bound language model and maps its requested calls back to the selected OpenAI wire format. Sophy never executes a tool. Your application runs it, then sends the result and full conversation state on the next call.

tools = [{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "Get weather for a city",
        "parameters": {
            "type": "object",
            "properties": {"city": {"type": "string"}},
            "required": ["city"],
        },
    },
}]

first = client.chat.completions.create(
    model="sophy",
    tools=tools,
    messages=[{"role": "user", "content": "Weather in Paris?"}],
)
call = first.choices[0].message.tool_calls[0]

second = client.chat.completions.create(model="sophy", tools=tools, messages=[
    {"role": "user", "content": "Weather in Paris?"},
    first.choices[0].message,
    {"role": "tool", "tool_call_id": call.id, "content": "18°C, sunny"},
])

Only function tools are supported; other Responses tool types are ignored. Chat uses the nested { type: "function", function: { ... } } shape; Responses uses the flat { type: "function", name, parameters } shape. Legacy Chat functions returns 400 functions_unsupported.

Tools and structured output are mutually exclusive

A key with a JSON Schema rejects tools with 400 tools_unsupported because both features compete for the model’s output channel.

Structured output

An operator can attach a JSON Schema to a language key. Sophy normalizes the schema across supported model providers. Buffered calls use constrained generation, tolerant JSON extraction and retry, then validate the completed object; streaming uses schema-constrained generation with post-hoc validation.

The schema lives on the key. Client-sent Chat response_format and Responses text.format do not replace it. Successful JSON is returned as the assistant text, so parse choices[0].message.content or response.output_text.

Buffered and streaming failure behavior differs

A buffered response that still fails validation returns 502 schema_validation_failed. Streamed bytes cannot be retracted after a 200 begins; Sophy records a validation failure after completion, but the client may already have received non-conforming text.

Audio transcription

POSThttps://sophy.in/v1/audio/transcriptions

Bind the key’s primary model to a transcription model and send audio as multipart/form-data. Sophy returns raw text when no processing policy is set, or applies centrally managed instructions in a second language-model step and returns only that processed text.

Audio transcription request fields
FieldTypeNotes
fileFileRequired, non-empty, and at most 4 MiB (4,194,304 bytes).
modelstringOptional for wire compatibility and ignored. The key’s primary transcription model wins.
languagestringOptional two-letter ISO-639-1 language hint, such as en.
response_formatjsonOptional; defaults to json. Other OpenAI transcription formats are not supported in this version.
advanced transcription optionsunsupportedClient prompt, temperature, logprobs, stream, timestamps, diarization, include, chunking, and known-speaker fields are rejected with 400 unsupported_transcription_option.

Accepted audio formats are MP3/MPEG/MPGA, MP4/M4A, WAV, WebM, FLAC, and OGG. Sophy identifies the format from the file bytes instead of trusting its name or client-declared MIME type. Files outside that allowlist return 400 unsupported_audio_format; files over the limit return 413 file_too_large.

from openai import OpenAI

client = OpenAI(base_url="https://sophy.in/v1", api_key="mw_live_…")

with open("meeting.m4a", "rb") as audio:
    resp = client.audio.transcriptions.create(
        model="sophy",
        file=audio,
        language="en",
        response_format="json",
    )

print(resp.text)
{
  "text": "Decisions: launch on Friday and send the checklist today.",
  "processed": true,
  "language": "en",
  "duration": 8.4
}

Processing policy controls what the client receives

With a blank key system prompt, text and transcript are identical and processed is false. With a non-blank system prompt, the operator must also set params.transcriptProcessorModel to a language model. Sophy transcribes first, then applies the key-owned instructions to produce text. The raw transcript field is omitted so a client cannot bypass centrally managed processing such as redaction. language and duration appear only when the transcription provider returns them.

Compatibility boundary

This route returns JSON only. Client prompts, temperature, logprobs, streaming, timestamp granularities, diarization, chunking, known-speaker hints, subtitle text, and verbose transcription formats are not supported in this version.

One uploaded clip consumes one RPM slot and counts as one client request. Sophy attributes transcription and processing to their actual models, while the processor component does not add a second client request. If content logging is enabled on the key, Sophy stores the filename, byte count, language hint, raw transcript, and final text—but never the audio bytes. Turn content logging off when unprocessed text must not be retained.

Embeddings

POSThttps://sophy.in/v1/embeddings

Bind the key to an embedding model and use the standard OpenAI embeddings method. The key-owned model is returned in the response, and usage records input tokens and estimated cost.

Embeddings request fields
FieldTypeNotes
inputstring | string[]Required. One non-empty string or 1–2,048 non-empty strings.
encoding_formatfloat | base64Optional; defaults to float. Base64 contains little-endian float32 bytes.
dimensionsintegerOptional, 1–100,000, and supported only for openai/* embedding models. Provider limits may be narrower.
modelstringIgnored. The embedding model bound to the key wins.
userstringIgnored.

Pre-tokenized integer arrays are not portable across a gateway-bound model and return 400 token_input_unsupported.

from openai import OpenAI

client = OpenAI(base_url="https://sophy.in/v1", api_key="mw_live_…")

resp = client.embeddings.create(
    model="sophy",
    input=["First document", "Second document"],
    encoding_format="float",
)
print(resp.data[0].embedding)
{
  "object": "list",
  "data": [
    { "object": "embedding", "index": 0, "embedding": [0.012, -0.044, 0.008] },
    { "object": "embedding", "index": 1, "embedding": [-0.031, 0.017, 0.052] }
  ],
  "model": "openai/text-embedding-3-small",
  "usage": { "prompt_tokens": 5, "total_tokens": 5 }
}

Evaluation models

POSThttps://sophy.in/v1/evaluate

Evaluation models are not exposed through the OpenAI-compatible client methods, so call this native Sophy route directly with an HTTP client. Bind the key to an evaluation model — one from the Vercel AI Gateway catalog, such as TypeSafe Jev, or one of Cloudflare’s Clef models — send the state to judge together with a named set of typed questions, and Sophy returns one answer per question plus token usage.

Questions are judged against the same shared state, so keep each one self-contained. The call is a single buffered round trip; there is no streaming.

Evaluate request fields
FieldTypeNotes
statestring | object | arrayRequired. The shared material every question is asked about. At most 200,000 characters once serialized as JSON.
questionsobjectRequired. Maps your own question names to question objects, 1–32 entries. Every question needs a non-empty instructions string and a type of boolean, choice, or score. On Clef keys, names must be 1–100 letters, digits, "_", "." or "-".
providerOptionsobjectOptional provider-specific knobs, namespaced by provider. Only the namespace matching the key model’s provider is accepted. Clef keys accept no providerOptions.
assetsobjectOptional, for TypeSafe Jev (typesafe-ai/jev-1.13.0 and later) and Cloudflare Clef keys. Maps an asset key to { "type": "image", "format", "data_base64" }. Jev: 1–8 PNG or JPEG images; put each key in state, an instruction or criteria to refer to its image. Clef: 1–4 PNG, JPEG or WebP images, no reference needed (see Cloudflare Clef models below).
modelstringIgnored. The evaluation model bound to the key wins and is echoed in the response.

Question types and answers

Evaluate answer shapes
FieldTypeNotes
booleanprobabilitycriteria is optional; when present it must supply string true and false descriptions. Answers as { "type": "boolean", "probability" }.
choicechoice, probabilitiescriteria is an object mapping at least two option names to non-empty string descriptions (at most 255 options on Clef keys). Answers as { "type": "choice", "choice", "probabilities" }, one probability per option, plus "confidence" (0–1) when the model returns one.
scorescore, probabilitiescriteria is an array of at least two non-empty string labels ordered lowest to highest (at most 10 on Clef keys). Answers as { "type": "score", "score", "probabilities" }, plus "confidence" when the model returns one. probabilities maps each level index, from "0" for the lowest label, to its probability; score is probability-weighted, so it can fall between levels.

Requests are validated before any paid call. Missing or oversized state returns 400 missing_state or 400 state_too_large; a bad question set returns 400 missing_questions, 400 invalid_questions, 400 questions_empty, 400 too_many_questions, 400 invalid_question, 400 missing_instructions, 400 unsupported_question_type, or 400 invalid_criteria. Out-of-namespace knobs return 400 unsupported_provider_options, and a key whose model is known to be something other than an evaluation model — including Clairic and any unsupported @cf/ model — returns 400 model_not_evaluation. Clef keys add the checks described below.

import httpx

resp = httpx.post(
    "https://sophy.in/v1/evaluate",
    headers={"Authorization": "Bearer mw_live_…"},
    json={
        "state": {
            "question": "How do I reset my password?",
            "answer": "Open Settings and use the reset link we email you.",
        },
        "questions": {
            "grounded": {
                "type": "boolean",
                "instructions": "Is the answer supported by the question?",
            },
            "tone": {
                "type": "choice",
                "instructions": "Classify the tone of the answer.",
                "criteria": {
                    "formal": "Professional and impersonal.",
                    "casual": "Friendly and conversational.",
                },
            },
            "helpfulness": {
                "type": "score",
                "instructions": "Rate how helpful the answer is.",
                "criteria": ["useless", "partial", "complete"],
            },
        },
    },
    timeout=120,
)
print(resp.json()["answers"])
{
  "model": "typesafe-ai/jev",
  "answers": {
    "grounded": { "type": "boolean", "probability": 0.93 },
    "tone": {
      "type": "choice",
      "choice": "casual",
      "probabilities": { "formal": 0.18, "casual": 0.82 }
    },
    "helpfulness": {
      "type": "score",
      "score": 1.71,
      "probabilities": { "0": 0.04, "1": 0.21, "2": 0.75 }
    }
  },
  "usage": { "inputTokens": 412, "outputTokens": 36, "totalTokens": 448 }
}

Images (TypeSafe Jev)

Keys bound to TypeSafe Jev 1.13.0 or later can send still images in assets. Give each image a key, then use that key as a value in state and refer to it by its state name in your questions. Images must be PNG or JPEG, base64-encoded with no data: prefix, 280–1,568 pixels on each side, and already rotated upright (EXIF orientation is not applied). The whole request must stay under 4.5 MB. TypeSafe still labels image input experimental, so validate results before you depend on them.

curl https://sophy.in/v1/evaluate \
  -H "Authorization: Bearer mw_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "state": { "selfie": "selfie-1" },
    "assets": {
      "selfie-1": { "type": "image", "format": "jpg", "data_base64": "/9j/4AAQ…" }
    },
    "questions": {
      "groomed": {
        "type": "score",
        "instructions": "Rate the grooming of the person in `selfie`.",
        "criteria": ["poor", "acceptable", "good"]
      }
    }
  }'

No TypeSafe header needed

TypeSafe’s own API requires a TypeSafe-Danger-Zone-Images: true header on every image request. On Sophy, the assets field is the opt-in: when a request includes it, Sophy adds that header upstream for you. Sophy doesn’t forward client headers, so sending the TypeSafe header to Sophy has no effect.

Images only reach the model through assets. Base64 pasted into state or a question arrives as plain text, and the model can’t see it. TypeSafe’s box question type (image location, Jev 1.14) is not supported yet; use boolean, choice, or score.

Bad image input is rejected before any paid call with 400 assets_not_supported, 400 invalid_assets, 400 too_many_assets, 400 invalid_asset_key, 400 invalid_asset, 400 unsupported_asset_format, 400 invalid_asset_data, 400 asset_format_mismatch, or 400 unreferenced_asset. An image the model rejects, for example one outside the size range, returns 400 upstream_invalid_request with TypeSafe’s message.

Cloudflare Clef models

Keys can also bind Cloudflare Workers AI’s Clef decision models. Sophy calls them with the project’s own Cloudflare Workers AI credential, never through Vercel AI Gateway, so a Vercel AI Gateway outage doesn’t stop them. Send the same request as for any evaluation model: Sophy picks Cloudflare’s model selector from the key, sends boolean questions as Cloudflare’s noul type, and maps answers and usage back to the shape above.

  • @cf/cloudflare/clef — Clef, context window 65,536 tokens.
  • @cf/cloudflare/clef-flash — Clef Flash, context window 24,576 tokens.
  • @cf/cloudflare/clef-omni — Clef Omni, context window 65,536 tokens.

Sophy sends these models text and images only. Cloudflare’s video input, and Clef Omni’s audio input, are not supported yet.

Clef keys add Cloudflare’s limits to the checks above, still before any paid call. Question names must be 1–100 letters, digits, _, ., or -. Choice questions take 2–255 options and score questions 2–10 levels. providerOptions is not accepted. Sophy also estimates the request size the way Cloudflare does — the JSON it sends upstream, without images, at four characters per token — and returns 400 state_too_large when the estimate exceeds the model’s context window: about 98,000 characters of JSON on Clef Flash, while on Clef and Clef Omni the 200,000-character state limit usually applies first.

Dense text such as logs can use about two characters per token, so a request under the estimate can still exceed the model’s real limit. In testing, Clef and Clef Flash rejected such requests instead of shortening the state, and Sophy returns 400 upstream_invalid_request with Cloudflare’s message, such as “Request requires 100819 tokens; the maximum is 65529”. Clef Omni’s limit is also its context window, and over-long input is expected to be rejected the same way, but that hasn’t been verified. Keep the state well inside the context window.

curl https://sophy.in/v1/evaluate \
  -H "Authorization: Bearer mw_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "state": "Support ticket #4821 from a retail customer: our checkout page has returned HTTP 500 errors for every customer since 09:10 UTC. We are losing roughly $2,000 per minute in sales. Please escalate immediately.",
    "questions": {
      "urgent": {
        "type": "boolean",
        "instructions": "Is this support request urgent?",
        "criteria": {
          "true": "Needs action within the hour",
          "false": "Can wait in the normal queue"
        }
      },
      "team": {
        "type": "choice",
        "instructions": "Which team should handle this request?",
        "criteria": {
          "billing": "Payments, invoices, and refunds",
          "technical": "Outages, errors, and configuration",
          "sales": "Plans and upgrades"
        }
      },
      "severity": {
        "type": "score",
        "instructions": "How severe is the customer impact?",
        "criteria": ["No impact", "Minor", "Major", "Critical"]
      }
    }
  }'
{
  "model": "@cf/cloudflare/clef",
  "answers": {
    "urgent": { "type": "boolean", "probability": 0.8998 },
    "team": {
      "type": "choice",
      "choice": "technical",
      "probabilities": { "billing": 0.0605, "technical": 0.931, "sales": 0.0085 },
      "confidence": 0.8058
    },
    "severity": {
      "type": "score",
      "score": 2.9603,
      "probabilities": { "0": 0.0035, "1": 0.0035, "2": 0.0222, "3": 0.9708 },
      "confidence": 0.9239
    }
  },
  "usage": { "inputTokens": 381, "outputTokens": 0, "totalTokens": 381 }
}

Clef keys can send up to four still images in assets, in the same keyed shape as Jev: PNG, JPEG, or WebP, base64-encoded with no data: prefix, at most 4 MiB each and 8 MiB in total once decoded. Cloudflare also limits each image to 16 megapixels, which Sophy doesn’t check. The whole request must still stay under 4.5 MB, which binds first. Clef sees every image before the state, in the key order of assets; as in JavaScript, keys that are plain whole numbers, such as 2, come first in numeric order. An image key doesn’t need to appear in the state or a question, and Sophy doesn’t swap a key for its image, so refer to images by position, such as “the first image”.

curl https://sophy.in/v1/evaluate \
  -H "Authorization: Bearer mw_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "state": "Ticket #4821: checkout has returned HTTP 500 for every customer since 09:10 UTC. The first image is a screenshot from the customer.",
    "assets": {
      "screenshot": { "type": "image", "format": "png", "data_base64": "iVBORw0KGgo…" }
    },
    "questions": {
      "error_page": {
        "type": "boolean",
        "instructions": "Does the first image show an error page?"
      }
    }
  }'

Coming from Cloudflare’s API

Send Sophy’s shape rather than Cloudflare’s: boolean instead of noul, which returns 400 unsupported_question_type, and images in assets instead of images. A model field is ignored, because the key selects the model and Sophy sends Cloudflare’s selector itself. A top-level images, audio, or videos field returns 400 unsupported_media_field. Responses aren’t wrapped in Cloudflare’s result envelope, usage is camelCase, and score answers omit Cloudflare’s legend; read each level from the probability keys.

Sophy accepts a strict subset of Cloudflare’s question shapes. Instructions, choice descriptions, and score labels must be non-empty strings. Cloudflare’s null, object, and array forms return 400 missing_instructions or 400 invalid_criteria, so replace a null description with a short sentence.

Bad Clef input is rejected before any paid call with 400 invalid_question_id, 400 invalid_criteria, 400 unsupported_provider_options, 400 unsupported_media_field, 400 invalid_assets, 400 too_many_assets, 400 invalid_asset_key, 400 invalid_asset, 400 unsupported_asset_format, 400 invalid_asset_data, 400 asset_format_mismatch, 400 asset_too_large, 400 assets_too_large, or 400 state_too_large. A project without a healthy Cloudflare credential returns 503 project_gateway_unavailable, even when its Vercel AI Gateway credential is healthy. A Cloudflare account on the Workers Free plan that has used its daily free allocation returns 429 rate_limit_exceeded until 00:00 UTC, and Sophy doesn’t retry it.

One evaluate call consumes one RPM slot and is checked against the key’s monthly cost cap before the paid model call. When content logging is enabled on the key, Sophy stores the evaluated state, the questions, and the returned answers. For images it logs only each one’s format and size, never the image itself. Clef calls are priced from Cloudflare’s reported usage; see Limits and quota.

Compatibility boundary

This is a native Sophy route, not an OpenAI-compatible one: no OpenAI SDK method reaches evaluation models, so send plain HTTP. The client-sent model is accepted and ignored — the evaluation model bound to the key wins. Responses are buffered only, and upstream providerMetadata and Clef’s legend are not echoed back.

Extraction models (preview)

POSThttps://sophy.in/v1/extract

Research preview

Extraction runs on Cloudflare’s Clairic (@cf/cloudflare/clairic), which Cloudflare ships as a research preview. Sophy validates every request before the paid call but passes each answer through exactly as Clairic returns it, so answer fields can change between Clairic releases. As Cloudflare puts it, every answer is well formed, not necessarily correct: use confidence, p_null, and the p10–p90 interval to decide what to automate and what to send to review.

Extraction models read text and images and return typed values — text, numbers, dates, times, durations, lists, estimates, and forecasts — instead of free text. Like evaluation models, they are not exposed through the OpenAI-compatible client methods, so call this native Sophy route with an HTTP client. Bind the key to Clairic, send the state to read together with named, typed fields, and Sophy returns one answer per field plus token usage.

Calls use the project’s own Cloudflare Workers AI credential, never Vercel AI Gateway. The call is a single buffered round trip; there is no streaming.

Extract request fields
FieldTypeNotes
statestringThe text to read, at most 200,000 characters counted as UTF-16 code units (JavaScript string length, so an emoji counts as two), and within the context-window estimate described below. Required unless assets carries images; with images it may be "" or omitted. Objects and arrays are not accepted: serialize them first.
fieldsobjectRequired. Maps your own field names to field specs, 1–64 entries. A name starts with a letter or "_" and has at most 64 letters, digits, "_", "." or "-". Answers come back under the same names.
assetsobjectOptional. 1–4 images in the same keyed shape and limits as Clef keys on /evaluate: PNG, JPEG or WebP, at most 4 MiB each and 8 MiB in total once decoded, no reference needed.
reference_datestringOptional ISO date (YYYY-MM-DD) that anchors dates written without a year. Sophy never fills it in, and the model does not know when the request runs, so send it whenever a field depends on today’s date.
optionsobjectOptional. include_quantiles (boolean) adds all 99 percentiles to estimates. return_state_text (boolean) adds state_text, the text that spans index into. No other keys.
modelstringIgnored. The extraction model bound to the key wins and is echoed in the response.

Field types

Extract field types
FieldTypeNotes
textstringText copied from the input, with its span.
numbernumberA number, read or computed from the input. Optional unit, integer, and decimals.
datestringAn ISO date such as 2026-10-29.
timestringA time of day such as 14:05:00.
datetimestringAn ISO date and time such as 2026-10-19T14:05:00, with no time zone.
durationnumberA length of time in unit: seconds (the default), minutes, hours, days, weeks, months, or years.
listarrayEvery matching value, in input order. Requires items: text, number, or date. Empty when nothing matches.
estimatedistributionA prediction inside min and max, returned as a distribution. Requires min and max (numbers, or ISO dates for a date estimate); optional step and scale.
forecastdistribution[]Future values of a series, one per step. Requires numeric min and max; optional horizon, interval, and scale.

Each field spec has a type and may add any of these settings; a setting not listed here returns 400 invalid_field.

Extract field settings
FieldTypeNotes
typestringRequired. One of the nine types above.
instructionsstringWhat the field should contain, for example “Amount due including the surcharge”.
unitstringA non-empty unit label such as USD. Duration fields accept only seconds, minutes, hours, days, weeks, months, or years.
integerbooleanNumbers and durations: return a whole number.
decimalsintegerNumbers, durations, and number lists: round to 0–6 decimal places.
itemsstringLists: text, number, or date. Required for lists.
min / maxnumber | dateRequired for estimates and forecasts, with min below max; ISO dates only for a date estimate. Optional numbers on other types.
stepnumber | stringEstimates: the grid, a positive number (days for a date estimate); "month" or "year" only on a date estimate.
scalestringEstimates and forecasts: linear or log.
null_thresholdnumberGreater than 0 and at most 1 (Clairic’s default is 0.5). When the probability that the input does not determine the field exceeds it, value is null.
horizonintegerForecasts: the number of steps, 1–64.
intervalstringForecasts: step, minute, hour, day, week, month, quarter, or year.

Requests are validated before any paid call. A key whose model is not an extraction model, a Clef key included, returns 400 model_not_extraction. A bad body returns 400 invalid_state, 400 missing_state, 400 state_too_large, 400 missing_fields, 400 invalid_fields, 400 fields_empty, 400 too_many_fields, 400 invalid_field_name, 400 unsupported_field_type, 400 invalid_field, 400 invalid_reference_date, or 400 invalid_options, and param names the problem, such as fields.total.decimals. Field specs may use only the settings above. Sophy checks each setting’s type and range, requires items for lists and min and max for estimates and forecasts, and leaves the rest to Clairic, whose rejections return 400 upstream_invalid_request with its message.

Sophy also estimates the request size the way Cloudflare does — the JSON it sends upstream, meaning the state and field specs without images, at four characters per token — and returns 400 state_too_large when the estimate exceeds Clairic’s 65,536-token context window, about 262,000 characters of JSON. JSON writes each newline, tab, quote, and backslash as two characters (other control characters as six), so a newline-dense series, tab- or quote-heavy CSV/TSV, or serialized JSON can reach that limit below 200,000 characters.

import httpx

resp = httpx.post(
    "https://sophy.in/v1/extract",
    headers={"Authorization": "Bearer mw_live_…"},
    json={
        "state": (
            "Invoice INV-7731 from Blue Harbor Logistics. Issued 14 Oct. Net 15. "
            "Freight: 3 pallets at $410.00 each; fuel surcharge $57.50. "
            "Contacts: Meera Iyer, Tom Okafor."
        ),
        "reference_date": "2026-10-20",
        "fields": {
            "invoice_number": {"type": "text", "instructions": "Invoice number"},
            "total": {
                "type": "number",
                "unit": "USD",
                "decimals": 2,
                "instructions": "Amount due including the surcharge",
            },
            "due_date": {"type": "date", "instructions": "Issue date plus the net payment days"},
            "contacts": {"type": "list", "items": "text", "instructions": "Contact person names"},
            "po_number": {"type": "text", "instructions": "Purchase order number"},
            "days_to_pay": {
                "type": "estimate",
                "min": 0,
                "max": 90,
                "step": 1,
                "instructions": "Days after issue until this invoice is paid",
            },
        },
    },
    timeout=120,
)
print(resp.json()["answers"])
{
  "model": "@cf/cloudflare/clairic",
  "answers": {
    "invoice_number": {
      "type": "text",
      "p_null": 0.0001,
      "span": [8, 16],
      "guess": "INV-7731",
      "confidence": 1,
      "value": "INV-7731"
    },
    "total": {
      "type": "number",
      "p_null": 0.0003,
      "guess": 1287.5,
      "confidence": 0.8683,
      "value": 1287.5
    },
    "due_date": {
      "type": "date",
      "p_null": 0.00001,
      "guess": "2026-10-29",
      "confidence": 0.9999,
      "value": "2026-10-29"
    },
    "contacts": {
      "type": "list",
      "value": ["Meera Iyer", "Tom Okafor"],
      "spans": [[137, 147], [149, 159]],
      "confidence": 0.9708
    },
    "po_number": {
      "type": "text",
      "p_null": 0.999999,
      "span": [8, 16],
      "guess": "INV-7731",
      "confidence": 0.9993,
      "value": null
    },
    "days_to_pay": {
      "type": "estimate",
      "value": 18,
      "mean": 17.58,
      "median": 16,
      "p10": 13,
      "p90": 25,
      "p_below": 0.000009,
      "p_above": 0.000009
    }
  },
  "truncated": false,
  "usage": { "inputTokens": 715, "outputTokens": 0, "totalTokens": 715 }
}

Reading answers

Extract answer shapes
FieldTypeNotes
textvalue, spanvalue is the text found in the input, or null; span is its [start, end) offset pair. Also guess, p_null, and confidence.
number, durationvalueA number, or null. Duration answers also carry unit. Also guess, p_null, and confidence.
date, time, datetimevalueAn ISO string such as 2026-10-29, 14:05:00, or 2026-10-19T14:05:00, or null. Also guess, p_null, and confidence.
listvalueAn array, empty when nothing matches, with an overall confidence. Number and date lists add confidences, one per item; text lists add spans, one per item.
estimatevalue, distributionvalue plus mean, median, an 80% interval from p10 to p90, p_below, and p_above; value can differ from median. include_quantiles adds quantiles, all 99 percentiles.
forecastarrays, stepsvalue, median, p10, and p90 are arrays with one entry per step; steps holds one estimate per step.
Extract response fields
FieldTypeNotes
modelstringThe extraction model bound to the key.
answersobjectOne answer per requested field, keyed by your field names, passed through from Clairic.
truncatedbooleanClairic’s truncation flag; false unless Clairic reports true.
state_textstringOnly when options.return_state_text is true: the text that spans index into.
usageobjectinputTokens, outputTokens, and totalTokens. Clairic may read the state more than once (the example above used 3 passes), and inputTokens counts every pass, so cost isn’t predictable from the field count alone.

Three rules for reading answers

Spans are end-exclusive offsets counted in Unicode code points, not bytes or UTF-16 units. In Python, state[start:end] returns the text. In JavaScript, use Array.from(state).slice(start, end).join(''), because state.slice(start, end) drifts after an emoji or any other character outside the Basic Multilingual Plane. Set options.return_state_text to receive the exact text the spans index into.

When value is null, ignore guess and span, even when confidence is high: the model found that the input doesn’t determine the field, and the guess can point at unrelated text. In the example above, an invoice with no PO number returns its invoice number as the PO guess. p_null is the probability that the input doesn’t determine the field.

List answers can carry confidences, one per item in value, alongside the list’s overall confidence; check each item before you use it. Text lists carry per-item spans instead.

Images

Send up to four still images in assets, exactly as for Clef keys on evaluate: PNG, JPEG, or WebP, base64-encoded with no data: prefix, at most 4 MiB each and 8 MiB in total once decoded, and a whole request under 4.5 MB. Clairic sees them before the state, in key order, and no reference is needed. With images, state may be empty or omitted. A top-level images, audio, or videos field returns 400 unsupported_media_field, and bad images return the same asset codes as Clef keys.

curl https://sophy.in/v1/extract \
  -H "Authorization: Bearer mw_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "assets": {
      "receipt": { "type": "image", "format": "jpg", "data_base64": "/9j/4AAQ…" }
    },
    "fields": {
      "merchant": { "type": "text", "instructions": "Store name" },
      "paid": { "type": "number", "unit": "USD", "decimals": 2, "instructions": "Total paid" },
      "paid_on": { "type": "date", "instructions": "Purchase date" }
    }
  }'

One extract call consumes one RPM slot and is checked against the key’s monthly cost cap before the paid model call; its cost is estimated as described in Limits and quota. When content logging is enabled on the key, Sophy stores the state, the field specs, the reference date and options, and the returned answers. For images it logs only each one’s format and size, and it never logs state_text. A project without a healthy Cloudflare credential returns 503 project_gateway_unavailable, even when its Vercel AI Gateway credential is healthy. A Cloudflare account on the Workers Free plan that has used its daily free allocation returns 429 rate_limit_exceeded until 00:00 UTC, and Sophy doesn’t retry it.

Compatibility boundary

This is a native Sophy route, not an OpenAI-compatible one, and not Cloudflare’s REST shape either: send plain HTTP with your Sophy key, put images in assets, and read answers directly — there is no result envelope. The client-sent model is accepted and ignored; the extraction model bound to the key wins. Responses are buffered only, and Clairic’s per-pass usage breakdown is not echoed back.

Image generation

POSThttps://sophy.in/v1/images/generations

Bind the key to a supported image model. Generated images are returned inline as base64 and are not stored by Sophy.

Image generation request fields
FieldTypeNotes
promptstringRequired and non-empty.
nintegerOptional, from 1 to 10.
sizestringOptional WIDTHxHEIGHT syntax with 2–5 digits per side, such as 1024x1024; provider support varies.
response_formatb64_jsonOptional. b64_json is the only supported format; url is rejected.
quality / stylestringOptional provider-specific options.
background / output_formatstringOptional provider-specific options; unsupported values may be ignored upstream.
model / userstringIgnored. The image model bound to the key wins.
from openai import OpenAI
import base64

client = OpenAI(base_url="https://sophy.in/v1", api_key="mw_live_…")

resp = client.images.generate(
    model="sophy",
    prompt="A red panda coding at a desk, watercolor",
    n=1,
    size="1024x1024",
)
with open("out.png", "wb") as f:
    f.write(base64.b64decode(resp.data[0].b64_json))
{
  "created": 1735689600,
  "data": [{ "b64_json": "iVBORw0KGgoAAAANSUhEUgAA…" }]
}

Files

POSThttps://sophy.in/v1/files

Upload a file as multipart/form-data using the field name file. Success returns 201. Use the returned URL in a Chat file/image part or a Responses input_file/input_image part.

Accepted content types are PDF, PNG, JPEG, WebP, GIF, plain text, CSV, and JSON. The maximum size is 4 MiB (4,194,304 bytes). Uploads become eligible for batched cleanup after 24 hours, so they are request staging—not durable file storage or a precise expiry service.

curl https://sophy.in/v1/files \
  -H "Authorization: Bearer mw_live_…" \
  -F "file=@./invoice.pdf"
{
  "id": "https://…/uploads/<project>/<key>/invoice-….pdf",
  "url": "https://…/uploads/<project>/<key>/invoice-….pdf",
  "pathname": "uploads/<project>/<key>/invoice-….pdf",
  "filename": "invoice.pdf",
  "bytes": 48213,
  "contentType": "application/pdf"
}

Upload URLs are public but unguessable so model providers can fetch them; treat each URL as a bearer URL, not a secret store. When a Sophy URL is submitted back through the API, Sophy checks its issuing key, and cross-key reuse returns 403 file_access_denied.

Models

GEThttps://sophy.in/v1/models

Returns exactly the primary model currently bound to the authenticated key in OpenAI list shape. This route does not return the full console catalog or an optional transcript processor, and it does not select a model for a later request.

{
  "object": "list",
  "data": [
    {
      "id": "anthropic/claude-sonnet-4.6",
      "object": "model",
      "created": 0,
      "owned_by": "sophy"
    }
  ]
}

Key-owned policy

The key is Sophy’s unit of configuration. Operators can change its policy without changing or redeploying the client. The next request reads the latest values.

Configuration stored on a Sophy key
FieldTypeNotes
modelmodel idOne primary model id. Use API routes supported by its catalog capabilities.
system promptstringAuthoritative prompt for language calls and optional transcript processing. Client prompts are dropped or rejected.
transcript processor modellanguage model idOptional params.transcriptProcessorModel. Required when a transcription key has a non-blank system prompt.
generation paramsobjectTemperature, top-p, and max output tokens for language calls, including transcript processing.
output schemaJSON SchemaOptional structured output policy for language calls.
knowledgebasereferenceOptional grounding source for Chat and Responses.
RPM / monthly capnumberOptional request-rate and proxy-traffic cost controls.
content loggingbooleanCaptures Chat/Responses text and replies, embedding inputs, transcription text, and evaluate and extract requests and answers for 30 days. Complete Chat/Responses image input values are discarded after 7 days and replaced by placeholders. Evaluate and extract images are logged as format and size only. Audio bytes and generated images are never stored.
owner / statuspolicyConsole scope, active/revoked views, rotation, and immediate revocation.

Client values for model and generation parameters are silently ignored. Chat and Responses prompt behavior is controlled separately by Agent mode. Audio transcription rejects a client prompt; only the key-owned system prompt can request post-processing.

When a language or transcript-processing call has an effective system prompt, Sophy prepends a fixed platform security preamble before the key and any applicable Agent-mode client instructions. On Anthropic models, Chat and Responses requests mark that system prompt (with any tools) as an ephemeral prompt-cache breakpoint, so requests on the same key within the cache window re-read it instead of paying full input price; knowledgebase context is sent after the breakpoint so it doesn’t break the shared prefix. Multi-turn requests on keys without a knowledgebase also mark the latest message, caching the conversation so far. Prompts shorter than the model’s cacheable minimum are not cached. Cache reads appear in usage as prompt_tokens_details.cached_tokens (Chat) and input_tokens_details.cached_tokens (Responses).

Agent mode

Standard keys are governed mode: Sophy drops client system and developer messages plus Responses instructions, then uses the key-owned prompt.

For a trusted server-side agent whose instructions must vary per request, an operator can enable Agent mode on the key. Sophy keeps the key prompt ahead of client instructions (after Sophy’s fixed security preamble), then appends:

  • Chat: consecutive leading system/developer messages, up to the first non-system turn.
  • Responses: instructions, followed by leading system/developer input items.

Later system-role items in the conversation are still dropped. Model, generation parameters, schema, knowledge, budgets, and rate limits remain key-owned.

Trust boundary

Enable Agent mode only for server-side applications that construct their own message array. Client instructions become operator-level input; never forward end-user-authored leading system/developer content on an Agent-mode key.

Knowledgebases

Project Admins can manage every project knowledgebase. Editors can create and manage knowledgebases they own. Both roles can build them from PDF, DOCX, Markdown, plain text, CSV, and JSON documents up to 4 MiB each. Sophy extracts, chunks, and embeds sources in the background.

Knowledgebases are shareable inside their project. An Admin can attach one to any project key, while an Editor can attach one only to a key they own. The same collection can ground multiple permitted keys.

On Chat or Responses requests with usable user text, Sophy embeds the latest user text (capped at its first 8,000 characters), retrieves the six closest chunks, and adds them to the effective prompt. Image- or file-only turns have no retrieval query. A transient retrieval timeout can continue without grounding. Missing, invalid, or billing-blocked project Gateway credentials fail closed before the model call; they never fall back to Sophy’s deployment credentials or another project.

Knowledgebase ingestion and query-embedding spend is tracked separately from client proxy traffic in the console.

Operator console

One passwordless identity can belong to multiple projects with an independent Admin or Editor role in each. The project in the URL is the tenant boundary, and every read, mutation, key, knowledgebase, log, evaluation, and audit event stays inside it.

Models

Search and compare the catalog — Vercel AI Gateway models plus Cloudflare’s Clef and Clairic models — by provider, type, capabilities, context window, and estimated pricing.

Projects and provider access

Switch projects, choose a personal default, invite members, rename the project, and connect, rotate, or disconnect its encrypted Vercel AI Gateway and Cloudflare Workers AI credentials.

Sophy keys and knowledge

Create and manage keys and knowledgebases within your role, then attach project knowledgebases to keys you are allowed to manage.

Usage and logs

Filter usage and request logs by key. Track requests, tokens, estimated cost, latency, model mix, errors, and source-tagged spend.

Champion vs challenger evals

Shadow successful live text requests, blind-judge results, and compare quality, cost, latency, and projected impact.

Champion/challenger run behavior and privacy

Evals observe eligible successful text requests only; requests containing media/file content or supplying tools are skipped. While a run is active, replayable sample content is temporarily captured even when normal content logging is off, then purged when the run ends. Sophy recommends a winner but never switches the key automatically.

Operator sign-in with email codes

The console and CLI use the same email identity. Sophy sends a six-digit one-time password (OTP) to your inbox. Each code expires after 10 minutes and permits at most five attempts. A successful attempt consumes the code.

Sign-in links no longer authenticate an account. Invitation links identify the invitation, but acceptance requires sign-in with the matching email account.

POST/api/admin/login
{ "email": "operator@example.com" }
{ "ok": true, "challengeId": "<challenge-id>" }

This response does not reveal whether an account exists. Email delivery and verification are subject to rate limits. When Sophy issues a new code, it invalidates the previous code for that email. The optional next field preserves a local invitation path through sign-in.

Code requests require 60 seconds between deliveries. Each email permits five codes per 15 minutes and 10 per hour. Each IP permits 10 requests per 15 minutes for each sign-in endpoint. Sign-in JSON bodies permit at most 4 KiB.

POST/api/admin/login/verify
Email code verification fields
FieldTypeNotes
challengeIdstringRequired. The challengeId from the code request.
codestringRequired. The six digits from the email, including any leading zero.
clientweb | cliRequired. Selects a browser cookie or a CLI session token.
nextstringOptional local return path for the web flow. External URLs are not allowed.
{ "challengeId": "<challenge-id>", "code": "012345", "client": "cli" }

For web, verification sets the browser session cookie and returns the next local path. For cli, it returns token, expiresAt, and user. A CLI session expires 30 days after its last use: each authenticated management request extends it to 30 days from that request, so a CLI in regular use stays signed in. Each request checks the current account, project membership, and project status.

POST/api/admin/cli/logout

Send the CLI token in the Authorization: Bearer header to revoke that session. The CLI removes its local session after a successful logout.

Sign-in endpoints return { "error": "code" } on errors. Malformed verification requests return 400 invalid_request. Invalid emails return 400 invalid_email. Invalid, used, or expired codes return 401 invalid_or_expired_code. Request limits return 429 too_many_attempts. An invalid CLI logout token returns 401 unauthorized.

Sophy CLI

Manage Sophy from a terminal or a coding agent that can run terminal commands. The CLI uses the same project roles and resource ownership as the console. Admins manage project resources. Editors manage the keys and knowledgebases they own.

Install the public @sophyai/sophy-cli npm package. A source checkout is not required.

Use Node.js 22 or newer. Install the CLI from npm:

npm install -g @sophyai/sophy-cli
sophy --help

To run a command without a global installation, use npx:

npx --package @sophyai/sophy-cli sophy --help
npx --package @sophyai/sophy-cli sophy login --email operator@example.com

The following examples use the installed sophy command. With npx, prefix each command with npx --package @sophyai/sophy-cli.

Sign in with your email. Then choose a project from your project list:

sophy login --email operator@example.com
sophy whoami
sophy projects list
sophy projects use <project-id>
sophy keys list

Enter the six-digit email code at the hidden login prompt. The default server is https://sophy.in. The installed command and npx use the same local session. The --project flag selects a project for one command. Run sophy logout to revoke the session.

For development from a source checkout, run npm install -g ./cli from the repository root.

Update key instructions

The systemPrompt field stores the instructions for a key. An update applies to subsequent requests with that key. Read the current settings before you change them:

sophy keys get <key-id> --project <project-id> --json

For a short instruction, use the --system-prompt flag:

sophy keys update <key-id> --project <project-id> \
  --system-prompt "Answer support questions using the attached knowledgebase."

For longer instructions, save a JSON file named key-instructions.json. Use \n for line breaks inside the JSON string:

{
  "systemPrompt": "You are the support assistant.\nUse the attached knowledgebase.\nIf the answer is unavailable, say so."
}

Apply the file. Then read the key again to confirm the saved instructions:

sophy keys update <key-id> --project <project-id> \
  --data @key-instructions.json --json
sophy keys get <key-id> --project <project-id> --json

Change only the fields you intend to change

An update preserves omitted settings. The example changes only systemPrompt. A supplied params object replaces all existing parameters. For a parameter change, include every parameter you want to retain. JSON null clears the system prompt.

Use Sophy with coding agents

A coding agent can use the CLI through its terminal. Commands run with the account signed in to that environment. A remote agent needs its own login in its environment. Your browser session or another computer's CLI session does not sign in that agent.

Use --json for compact JSON output. Use --data @file.json for nested input or long prompts. Project roles and resource ownership still apply to every command. Admin-only changes remain restricted to project admins.

Copy these instructions into a task for your coding agent:

Use the Sophy CLI for this task.

1. Run sophy --help, then sophy keys update --help to read the supported fields.
2. Run sophy whoami --json. If sign-in is required, ask me to complete
   sophy login in this environment. Do not copy a session from another machine.
3. Run sophy projects list --json. Use the project ID and key ID I identify.
   Include --project <project-id> on every project command.
4. Before editing, run:
   sophy keys get <key-id> --project <project-id> --json
5. Put only the requested changes in changes.json. Use systemPrompt for instructions.
   Preserve omitted settings. If changing params, include all parameters to retain.
6. Apply the changes:
   sophy keys update <key-id> --project <project-id> --data @changes.json --json
7. Read the key again with keys get. Confirm the requested changes and preserved settings.
   Report the saved result. Treat any nonzero exit code as incomplete work.
8. Use --yes only for an action I explicitly authorized. Do not include secrets in reports.

The CLI returns exit code 1 for errors. Exit code 2 means a bulk action skipped items or a key update requires evaluation-stop confirmation. The JSON result identifies the incomplete work.

CLI commands and management API

Discover commands, command groups, and input fields through the built-in help:

sophy --help
sophy keys --help
sophy keys update --help
CLI management coverage
FieldTypeNotes
keysmanagementList, create, edit, rotate, and revoke. Change models or start evaluations in bulk.
key settingspolicySet the model, prompt, parameters, schema, logging, RPM limit, knowledgebase, and transcript processor. Admins also control budgets and ownership.
projects / gatewaymanagementList, create, rename, and select projects. Admins connect or disconnect the project Gateway credential.
cloudflaremanagementAdmins connect, replace, or disconnect the project Cloudflare Workers AI credential. The API token is read only from a file or stdin. Editors see only its connection state and whether it is ready.
members / invitationsadmin onlyManage project membership, roles, and pending invitations.
settingsadmin onlyRead or update the project judge model and notification email.
knowledgebasesmanagementCreate and remove collections. List, upload, retry, and remove documents within your access.
evals / models / usage / logsoperationsRun or cancel evaluations. Read results, the model catalog, usage, and request logs within your access, overall or for one key.

The CLI accepts credential secrets only from a file or standard input, never as command-line arguments. Put {"accountId": "…", "apiToken": "…"} in a file and run sophy cloudflare connect --data @cloudflare.json, or pipe it with --data -. You can pass the account ID as --account-id instead and keep only the token in the JSON. The Vercel AI Gateway key uses sophy gateway connect the same way, with {"apiKey": "…"}. The cloudflare commands require @sophyai/sophy-cli 0.3.0 or later.

Sophy checks the Cloudflare account ID and token with Cloudflare before it saves them, and a failed check leaves the current credential in place. A failed connection returns one of these errors:

  • 400 cloudflare_invalid_account_id: Enter the 32-character Cloudflare account ID.
  • 400 cloudflare_invalid: The Cloudflare account ID or API token was rejected. Create the token from Cloudflare's "Workers AI" template.
  • 503 cloudflare_unreachable: Cloudflare could not be reached to validate the credential. Try again.
  • 409 cloudflare_in_use: This Cloudflare API token is already in use by another project.

Creating a key for a Cloudflare model, or changing a key to one, returns 409 cloudflare_not_ready (“Connect a healthy project Cloudflare credential first.”) until that credential is ready. An unsupported @cf/ model returns 400 invalid_request with the message “Unsupported Cloudflare Workers AI model”.

Key creation and rotation show the new secret once. Sophy cannot return an existing secret. Rotation and revocation require confirmation.

To read one key's recent traffic, supply its ID. The command returns the key's 100 most recent logs from every log source, newest first. Add --source or a smaller --limit to narrow the list:

sophy logs list --key-id <key-id> --project <project-id> --json
sophy logs get <log-id> --project <project-id> --json

A log list contains metadata. logs get returns one log with any retained content. Admins can read logs for any key in the project, and editors for the keys they own. Any other key returns 404 not_found. The management API takes the key as keyId in the logs.list input.

POST/api/admin/cli

The management API requires a CLI session token in the Authorization: Bearer header. Browser cookies and application API keys cannot authenticate this route. CLI tokens cannot authenticate the AI routes under /v1.

CLI management request fields
FieldTypeNotes
operationstringRequired. An operation name, such as keys.list.
projectIdUUIDRequired for project operations. The server checks your active membership.
inputobjectOperation-specific fields. The server validates types and rejects unknown fields.
{ "operation": "keys.list", "projectId": "<project-id>", "input": {} }
{ "ok": true, "data": [], "sessionExpiresAt": "2026-11-21T09:30:00.000Z" }
{ "error": { "code": "forbidden", "message": "Your current project role or resource ownership does not allow this action." } }

sessionExpiresAt is the session's current expiry after this request's renewal. The server records renewals at most once a day, so it can trail the request time plus 30 days by up to one day. Logout and revoked or expired sessions are never renewed.

Management responses use Cache-Control: no-store. Access decisions happen on the server. A local project choice or a stale CLI session never grants additional project rights.

JSON requests permit at most 1 MiB. Knowledgebase uploads use multipart fields operation=knowledgebases.upload, projectId, kbId, and file. Files permit at most 4 MiB, with 64 KiB for the multipart envelope. Each account permits 120 management requests per minute.

Invalid fields return 400 invalid_request. Missing project selection returns 400 project_required. Unknown operations return 400 unknown_operation. Invalid sessions return 401 unauthorized, denied access returns 403 forbidden, and unavailable resources return 404 not_found.

Oversized requests return 413 payload_too_large. Unsupported content types return 415 unsupported_media_type. Request limits return 429 rate_limited with Retry-After: 60. Unexpected failures return 500 internal_error without database details.

Errors and retries

Before a response stream starts, Sophy-generated failures use the OpenAI error envelope below with Cache-Control: no-store. Field validation errors may use code: null and identify the field in param.

{
  "error": {
    "message": "Invalid API key.",
    "type": "authentication_error",
    "param": null,
    "code": "invalid_api_key"
  }
}
Named Sophy API error codes
StatusTypeCodeWhen
400invalid_request_errorfunctions_unsupportedLegacy Chat Completions `functions` was sent; use `tools`.
400invalid_request_errorstateful_unsupportedResponses `previous_response_id` was sent; send the full input each call.
400invalid_request_errortools_unsupportedFunction tools were sent to a key configured for structured output.
400invalid_request_errorinvalid_content_typeAudio transcription was not sent as multipart/form-data.
400invalid_request_errorinvalid_multipart_formThe audio transcription multipart body could not be parsed.
400invalid_request_errormissing_fileThe transcription multipart body did not contain a `file` field.
400invalid_request_errorinvalid_fileThe transcription `file` field was not a valid uploaded file.
400invalid_request_errorempty_fileThe uploaded audio file contained no bytes.
400invalid_request_errorunsupported_audio_formatThe uploaded bytes were not recognized as an allowed audio format.
400invalid_request_errorinvalid_languageThe optional transcription `language` value was invalid.
400invalid_request_errorunsupported_transcription_optionA client `prompt`, streaming, or timestamp option was sent.
400invalid_request_errormodel_not_transcriptionThe key’s known model cannot handle buffered audio transcription, including realtime-only models.
400invalid_request_errortranscript_processor_requiredThe key has transcript-processing instructions but no processor model.
400invalid_request_errorprocessor_model_not_languageThe configured transcript processor is a known non-language model.
400invalid_request_errorinvalid_urlA Chat or Responses image/file part contains a malformed URL.
400invalid_request_errormodel_not_imageImage generation was requested with a known non-image key model.
400invalid_request_errorunsupported_response_formatImage output requested anything but `b64_json`, or transcription requested anything but `json`.
400invalid_request_errormodel_not_embeddingEmbeddings were requested with a known non-embedding key model.
400invalid_request_errormodel_not_languageChat Completions or Responses was called with a known non-language key model.
400invalid_request_errortoken_input_unsupportedPre-tokenized embedding input was sent; use strings.
400invalid_request_errorunsupported_encoding_formatEmbedding `encoding_format` was not `float` or `base64`.
400invalid_request_errordimensions_unsupported`dimensions` was sent to a non-OpenAI embedding model.
400invalid_request_errormodel_not_evaluationEvaluation was requested with a key whose model is known not to be an evaluation model, including Clairic and unsupported `@cf/` models.
400invalid_request_errormissing_state`/evaluate` was called without a `state` to judge, or `/extract` with an empty or missing `state` and no images.
400invalid_request_errorstate_too_large`state` exceeds 200,000 characters (counted after JSON serialization on `/evaluate`), or a Cloudflare model’s request is estimated above its context window.
400invalid_request_errormissing_questions`/evaluate` was called without a `questions` object.
400invalid_request_errorinvalid_questionsEvaluate `questions` was sent as something other than an object.
400invalid_request_errorquestions_emptyEvaluate `questions` contained no entries.
400invalid_request_errortoo_many_questionsEvaluate `questions` contained more than 32 entries.
400invalid_request_errorinvalid_questionAn entry in evaluate `questions` is not an object.
400invalid_request_errormissing_instructionsAn evaluate question has no non-empty `instructions` string.
400invalid_request_errorunsupported_question_typeAn evaluate question `type` is not `boolean`, `choice`, or `score`.
400invalid_request_errorinvalid_criteriaAn evaluate question’s `criteria` does not match the shape its type requires, or breaks a Clef limit: at most 255 choice options, each with a non-empty id, and at most 10 score levels.
400invalid_request_errorinvalid_question_idA question name on a Clef key is not 1–100 letters, digits, `_`, `.`, or `-`.
400invalid_request_errorunsupported_provider_optionsEvaluate `providerOptions` is not an object, addresses a namespace other than the key model’s provider, or was sent to a Clef key.
400invalid_request_errorunsupported_media_fieldA Clef evaluate or an extract request sent a top-level `images`, `audio`, or `videos` field; send images in `assets`.
400invalid_request_errorassets_not_supportedEvaluate `assets` was sent on a key whose model is neither TypeSafe Jev nor Cloudflare Clef.
400invalid_request_errorinvalid_assets`assets` is not an object, or has no entries.
400invalid_request_errortoo_many_assets`assets` has more than 8 entries on a TypeSafe Jev key, or more than 4 for a Cloudflare model.
400invalid_request_errorinvalid_asset_keyAn asset key is not 1–64 letters, digits, `_`, or `-`.
400invalid_request_errorinvalid_assetAn asset entry is not an object with `type: "image"`.
400invalid_request_errorunsupported_asset_formatAn asset `format` is not `png`, `jpg`, or `jpeg` (TypeSafe Jev), or not one of those or `webp` (Cloudflare models).
400invalid_request_errorinvalid_asset_dataAn asset’s `data_base64` is empty or not standard padded base64 (no `data:` prefix, no whitespace).
400invalid_request_errorasset_format_mismatchAn asset’s bytes are not the PNG, JPEG, or WebP image its `format` declares.
400invalid_request_errorasset_too_largeOne image for a Cloudflare model exceeds 4 MiB once decoded.
400invalid_request_errorassets_too_largeImages for a Cloudflare model exceed 8 MiB in total once decoded.
400invalid_request_errorunreferenced_assetA TypeSafe Jev asset key does not appear anywhere in `state` or the questions.
400invalid_request_errormodel_not_extraction`/extract` was called with a key whose model is not a supported extraction model (Clairic).
400invalid_request_errorinvalid_stateExtract `state` was a number, boolean, object, or array instead of a string.
400invalid_request_errormissing_fields`/extract` was called without a `fields` object.
400invalid_request_errorinvalid_fieldsExtract `fields` was sent as something other than an object.
400invalid_request_errorfields_emptyExtract `fields` contained no entries.
400invalid_request_errortoo_many_fieldsExtract `fields` contained more than 64 entries.
400invalid_request_errorinvalid_field_nameAn extract field name is not a letter or `_` followed by at most 63 letters, digits, `_`, `.`, or `-`.
400invalid_request_errorunsupported_field_typeAn extract field `type` is missing or not text, number, date, time, datetime, duration, list, estimate, or forecast.
400invalid_request_errorinvalid_fieldAn extract field spec is not an object, has an unknown setting or a value of the wrong type or range, or lacks `items` (list) or `min` and `max` (estimate, forecast).
400invalid_request_errorinvalid_reference_dateExtract `reference_date` is not a real `YYYY-MM-DD` date.
400invalid_request_errorinvalid_optionsExtract `options` is not an object, or has anything but boolean `include_quantiles` and `return_state_text`.
400invalid_request_errorunsupported_file_typeThe uploaded file content type is outside the allowlist.
400invalid_request_errorupstream_invalid_requestThe model provider rejected the request as invalid; its actionable detail is returned.
401authentication_errormissing_api_keyNo valid Bearer Authorization header was supplied.
401authentication_errorinvalid_api_keyThe key is malformed, unknown, revoked, expired, or belongs to an inactive project.
402insufficient_quotaquota_exceededThe key reached its monthly proxy-traffic cost cap.
402insufficient_quotainsufficient_quotaCloudflare Workers AI answered HTTP 402 for the project’s Cloudflare account. A Vercel AI Gateway credit or budget failure instead marks the credential billing-blocked and returns 503 `project_gateway_unavailable`, and a used-up Cloudflare daily free allocation returns 429 `rate_limit_exceeded`.
403invalid_request_errorfile_access_deniedA request references a Sophy upload owned by another key.
413invalid_request_errorfile_too_largeA file or audio upload exceeds 4 MiB (4,194,304 bytes).
429rate_limit_errorrate_limit_exceededThe key RPM limit or an upstream provider rate/quota limit was reached.
502api_errorschema_validation_failedA buffered structured response could not satisfy the key schema.
502api_errorupload_failedSophy could not persist a file upload.
502api_errorupstream_errorA transient upstream network or server failure occurred, or the model provider failed or returned an unusable response.
503api_errorproject_gateway_unavailableThe key is valid, but its project has no usable credential for the key model’s provider: Vercel AI Gateway, or Cloudflare Workers AI for `@cf/` models.

Upstream client-input rejections (400/413/422) become 400 upstream_invalid_request with a safe, capped message. Upstream rate limits preserve 429 and forward Retry-After when present. A valid Sophy key receives 503 project_gateway_unavailable when the project credential its model needs — Vercel AI Gateway, or Cloudflare Workers AI for Cloudflare models — is missing, disconnected, invalid, billing-blocked, not yet verified, or undecryptable. The message is provider-neutral and asks a project admin to connect or check the project’s AI provider credentials. Sophy makes no fallback upstream call: a Cloudflare model never falls back to Vercel AI Gateway, or the reverse. When Cloudflare rejects a stored token during a request, Sophy marks that credential invalid and returns the same 503 until an Admin reconnects it. Transient network and server failures are hidden behind 502 upstream_error.

Errors after streaming starts

Once an SSE response has begun with 200, a later model failure cannot be replaced by a JSON error envelope. Chat still attempts a final [DONE] but may lack its normal finish/usage chunks; Responses may omit response.completed.

Rate limits and monthly quota

Chat, Responses, audio transcription, embeddings, image generation, model evaluation, and extraction enforce the key’s optional requests-per-minute limit and monthly cost cap before the paid model call. /files and /models do not consume those request counters.

  • RPM uses a fixed 60-second window. Exceeding it returns 429 rate_limit_exceeded with Retry-After in seconds.
  • Monthly quota uses the current UTC calendar month and counts client-attributable spend, including transcript processing and the estimated cost of Cloudflare Workers AI calls. Reaching it returns 402 quota_exceeded.
  • Champion-versus-challenger evaluation and knowledgebase spend is recorded under separate sources for visibility and does not consume a key’s proxy-traffic cap.

Console cost values are estimates for operational decisions, not a billing-grade ledger. Vercel AI Gateway calls record the cost the gateway reports. Cloudflare Workers AI calls are estimated from the neurons Cloudflare reports for each call, at its published $0.011 per 1,000 neurons, or from the model’s listed input-token price when no neuron count comes back; output tokens are not billed. The estimate ignores Cloudflare’s free allocation of 10,000 neurons per account per day, so Cloudflare can bill less than Sophy records. A response that Cloudflare completed and billed but Sophy couldn’t use — for example, one missing a requested field, which returns 502 upstream_error — is still metered from the neurons Cloudflare reported for it. Requests that Cloudflare rejected record no cost.

Need a key or a policy change? Sign in to the Sophy console.