"OpenAI-compatible" is not a switch that a service either has or lacks. It is a claim about a set of behaviors, and those behaviors live on three separate layers: the protocol surface, where request and response shapes and authentication have to line up; the feature surface, where streaming, tool calling, structured output, and input modalities each get their own verdict; and the behavior surface, where error semantics, rate limits, and per-model parameter support decide whether an integration survives contact with production. Compatible is also not the same as equivalent: two endpoints can both deserve the label while differing in the details your application depends on.
This article walks each layer with a verification method, then condenses everything into an eight-item checklist you can run in an afternoon against any OpenAI-compatible endpoint, including the one at https://www.string.ink/v1 if that is your target.
Layer 1: The OpenAI-compatible protocol surface
The minimum common surface of an OpenAI-compatible API is small: a base URL that ends at /v1, Bearer token authentication in the Authorization header, POST /v1/chat/completions for conversations, and GET /v1/models for discovery. If your client can speak those four things, it can open a conversation with the endpoint, and that is the layer most "compatible" claims are really about.
The nuance worth internalizing is that Chat Completions and the Responses API are two different protocol shapes, not two names for one thing. The Chat Completions shape sends a messages array and answers with a choices array, and most third-party endpoints implement it as the common surface. The Responses shape uses a different request and event vocabulary. OpenAI's migration guide notes that the function-calling shape differs between them and that structured outputs use text.format in the Responses shape instead of response_format, while also stating plainly that "While Chat Completions remains supported, Responses is recommended for all new projects." Both shapes are current; they are simply not interchangeable at the wire level.
For integration work this produces one rule: match the shape your client sends to the shape the endpoint serves. Where the choice lives is often client-side. Codex CLI, for example, makes it explicit in its provider configuration through a wire_api setting. So when you evaluate an endpoint, start by writing down which shape your client speaks, then verify the endpoint answers in that shape. String AI's platform documentation describes its compatible surface directly: the base URL is fixed at https://www.string.ink/v1, authentication follows the OpenAI Bearer convention, and the interface list covers chat completions, text and vision inputs, image generation and editing, and the models list.
Layer 2: The feature surface, item by item
Protocol agreement is the entry ticket. Features are where differences accumulate, and each one gets its own test rather than a blanket assumption.
- Model list. Call GET /v1/models and confirm the response carries object "list" with a data array. This is also where you copy the model IDs you will use; the documentation's examples, such as gpt-5.6-luna and gpt-6-astra, are orientation, and the live list is the source of truth. The docs suggest validating the link with the lighter gpt-5.6-luna before moving to another model.
- Minimal chat completion. Send one user message and confirm the response contains the assistant's text where the Chat Completions shape puts it. This proves the request path end to end, and nothing else yet.
- Streaming. Turn on stream and read the Server-Sent Events sequence: each chunk arrives with object "chat.completion.chunk", incremental text shows up under choices[].delta.content, a final stats chunk usually carries usage, and the stream closes with a [DONE] marker. Never assume streaming works because non-streaming does; the event sequence is its own implementation.
- Tool calling. Pass tools and tool_choice, confirm the model returns a call in the expected shape, then complete the round trip by sending the tool result back and getting a follow-up answer. One-shot calls that never loop back hide the most common tool-calling defects.
- Structured output. Request a specific JSON shape and run it several times. The question is not whether the text parses once; it is whether the shape stays stable across runs and inputs.
- Vision input. Include an image in a message and confirm the model actually processes it. Text and vision inputs travel together on this endpoint, but that says nothing about every model behind it; verify per model.
- Image generation and editing. These are separate endpoints, not chat features. If your plan involves generating or editing images, test those routes on their own; a chat endpoint will not do the job.
Each bullet is a checkbox, and the box stays unchecked until you have seen the behavior yourself. Two minutes per item buys you a compatibility statement that is actually yours.
Layer 3: The behavior surface
The behavior layer is invisible in documentation samples and decisive in production. Read it as a set of symptom-driven probes.
Error shapes. 401, 403, 404, model-not-found, and 429 all mean different things, and clients frequently need to distinguish them to react correctly. Trigger each one deliberately: a wrong key, a malformed path, a bad model ID, and enough volume to see a rate-limit response if one appears. Record the status code and the body shape for each. If your client's retry logic keys off error types, this record is what it will run on.
Rate limiting and concurrency semantics. Limits are implementation behavior, not protocol. Find out how the endpoint signals throttling and what it expects your client to do next, then set your concurrency and backoff accordingly. The result differs across providers and can differ across keys; treat your measurement as the spec.
Parameter support varies by model. Temperature-style controls, reasoning-related parameters, and modality options are not uniform across every model an endpoint serves. A parameter that a flagship model accepts may be rejected elsewhere on the same base URL. Never assume parameters are endpoint-wide; gate them by model and verify per model you actually call.
The through-line of this layer: when a claim and an observation disagree, the observation wins. Write down what you saw, on which model, under which request, because the next person debugging the integration will need exactly that context.
The API compatibility checklist
Run these eight against the endpoint you are evaluating. Each produces a recorded artifact, not a feeling.
- List models with GET /v1/models and copy the current model IDs into your test config. If this is your first call ever, the five-step quickstart covers this step and the next one in detail.
- Send a minimal chat completion with a single user message and save the raw response.
- Re-run it with stream enabled and walk the event sequence from first chunk to the terminal marker.
- Execute a full tool-calling round trip: call, result, follow-up.
- Include an image in a request and confirm the model reads it.
- Request a strict structured output and confirm the shape holds across several runs.
- Inject three errors on purpose (bad key, bad path, bad model ID) and record what each one returns.
- Log a baseline: the raw response of each check with a timestamp, plus observed response times, so you can re-run the checklist later and compare. The method matters more than this run's numbers; keep the artifacts, not impressions.
Re-run the whole checklist after any endpoint or client upgrade, and after switching model IDs.
Three ways compatible gets misread
1. "It returned 200, so it is compatible." A 200 from a minimal chat request proves the protocol layer and nothing else. Streaming, tool calling, structured output, and modalities each fail independently, and each needs its own pass.
2. "This model accepted the parameter, so the endpoint does." Parameter support sits per model, not per endpoint. Treat every parameter as model-specific until your own check says otherwise.
3. "The sample uses one shape, my client uses the other." Documentation and examples come in both the Chat Completions and Responses shapes. Copying a text.format snippet into a Chat Completions client, or the reverse, produces errors that look like incompatibility but are really a shape mismatch. Match samples to the protocol your client actually speaks.
When compatibility is not enough
An honest compatibility statement is narrower than the label suggests, and that is fine as long as everyone knows the width. The verdict you record is a property of one endpoint implementation plus one client version; either side can upgrade and change it, which is why the checklist above is built to be re-run rather than trusted. And when a requirement falls outside the verified subset, decide consciously what to do about it: adapt the client, pick a different model on the same endpoint, or redesign around the limit.
Keep the live references live. Capabilities on any platform move forward, and the interface list in the platform documentation is the current map; your checklist is how you confirm the territory. One more rhythm to borrow from operations: model IDs age, and retirements eventually arrive, so plan for rerunning this process across model changes as well. The deprecation playbook covers that lifecycle directly.
Related reading
- Running Codex CLI, Claude Code, and Cursor on a single key: applying a verified endpoint across several tools instead of one client.
- Developer articles: integration notes and setup guides for the platform.