Migrate from Speakeasy
Speakeasy open-sourced its generator in September 2026 and repositioned around AI governance. You keep the SDKs you already generated — but the company that maintained the pipeline behind them sells something else now. Glotto picks it up, and adds the part that is hard to rebuild in-house: every regeneration is verified, not just emitted — compile and contract statuses, per-target drift, and custom-code preservation, recorded in a verification report delivered to CI, the release PR, and the console.
You can run this conversion yourself. glotto migrate speakeasy ships in the published CLI —
unlike migrating off Fern, where the conversion has to read your API
surface through the spec pipeline and stays on our side. Everything it needs is already in your git
repo: Speakeasy keeps its config in your tree, not in a dashboard, which is the same practical
advantage Fern has and Stainless does not.
What Glotto needs from you
Two files, at two levels — and the second level is the one that gets forgotten.
The root .speakeasy/workflow.yaml says what to generate and from where:
sources:
Acme Pets API (Public):
inputs:
- location: ./openapi/petstore.yaml
overlays:
- location: ./overlays/names.yaml
registry:
location: registry.speakeasyapi.dev/acme/acme/petstore-public
Acme Pets API (Internal):
inputs:
- location: registry.speakeasyapi.dev/acme/acme/petstore-internal
A .speakeasy/gen.yaml beside each generated SDK says what that SDK is called:
python:
version: 3.4.1
packageName: acme-petstore
moduleName: acmepets
Your package name and module namespace live only in that second file. The conversion finds them
by walking each target’s output path, so it reads the tree rather than the one file you point it
at. If a target’s gen.yaml is missing or unreadable, you get
GLOTTO_MIGRATE_SPEAKEASY_GEN_MISSING naming the exact path it looked for — the target is still
migrated from the workflow alone, but its package identity is not, and a partial migration never
passes itself off as a complete one.
How you get it
npm install -g @glotto/cli
glotto login
glotto migrate speakeasy
The defaults are the layout you already have: .speakeasy/workflow.yaml in, glotto.yml out.
--in and --out move either end, --force overwrites an existing config, and --report also
writes the report to a file.
If your workflow declares more than one source
It probably does — most real ones do. glotto.yml takes exactly one input document, so the
conversion stops and names them:
GLOTTO_MIGRATE_SPEAKEASY_SOURCE_AMBIGUOUS
Nothing is written. This is the one place the tool refuses rather than choosing, and it refuses on purpose: picking a source silently would regenerate your SDKs from a document you did not select, and nothing would surface it until one of your consumers broke. Re-run naming the one you want — quoted, because real source ids contain spaces and parentheses:
glotto migrate speakeasy --source 'Acme Pets API (Public)'
The message tells you which of the two real shapes you have. Several genuinely different documents (an Azure spec and a GCP spec) are several Glotto projects — migrate each separately. One document under per-language overlay stacks is one API, and you pick the stack you want as the baseline.
If a source is backed by the Speakeasy registry
A registry.speakeasyapi.dev/... input resolves against the service you are leaving, so carrying it
across would hand you a config that validates and then 404s in your CI. You get
GLOTTO_MIGRATE_SPEAKEASY_DEAD_REGISTRY instead.
Unlike the multi-source case above, this one does not stop — the rest of your workflow still
converts, and the conversion writes a glotto.yml with openapi.source: REPLACE_ME where the dead
reference would have gone. Export the document to a local path, fill that in, and you are done.
What you get back
A glotto.yml, and a report that gives every key you wrote exactly one of four verdicts. The
four are the whole point, and the third and fourth are where the value is:
| Verdict | What it means |
|---|---|
| Mapped | Translated. The line names the glotto.yml key it became. |
| Already guaranteed | You are not losing this — Glotto provides it unconditionally, so it needs no configuration. enableCustomCodeRegions becomes a three-way merge on every regeneration; forwardCompatibleEnumsByDefault is simply how Glotto emits enums. |
| Not honored | An emission-shape knob Glotto decides itself (maintainOpenAPIOrder, responseFormat, multipartArrayFormat), or vendor bookkeeping (management, features) that records generation checksums rather than anything you chose. Most of a real per-language block is this. |
| Unmapped | Genuinely no glotto.yml home, reported at its precise dotted path. This is the list that needs your attention, and it is short because the other three tiers are not in it. |
A migration report that called a guarantee a loss would read as a list of things you are giving up, when it is the list of things you no longer have to configure. Every code has a row on the migration diagnostics reference.
Your publish credentials are dropped, loudly
Your workflow carries registry tokens. A credential is never written into your glotto.yml —
not the value, not the key. The intent carries over — publish.pypi.token becomes
publish: { pypi: true }, so your release pipeline can be rebuilt — and the credential itself is
never written into a file you commit. You get GLOTTO_MIGRATE_SPEAKEASY_SECRET_DROPPED naming each
one, because a token dropped silently is a release pipeline that fails later for no stated reason.
Configure them as repository secrets for the release workflow instead.
Targets that are not SDK languages
Speakeasy lets a target be something other than an SDK language, and the three cases land in two different places.
terraform and cli become ordinary targets entries — Glotto generates a
Terraform provider and a CLI as targets, under those same
names. mcp-typescript does not: Glotto’s MCP server is its own top-level mcp block rather than a
language target, so the conversion routes it there. Mapping that one by its Speakeasy name would put
mcp-typescript under targets, where it is not a valid language, and you would find out at
validation rather than here.
What you still fill in by hand
A Speakeasy workflow carries no company identity, so the conversion cannot invent one. The report
ends with a placeholder count — two, on a typical project: your organization name and a contact
address. Each is written as an obvious stand-in (REPLACE_ME, an example.invalid address) rather
than a plausible guess, and each is listed in the report under Placeholders (needs review) rather
than left for you to discover.
Your API surface is not one of them. Your workflow points at an OpenAPI document, so Glotto reads
it and writes the resources block for you — operations grouped by their first tag, each keyed by
its operationId:
resources:
pets:
methods:
createPet: post /pets
getPet: get /pets/{petId}
listPets: get /pets
That key is the method name Glotto emits, taken from the same operationId your Speakeasy SDK
derived its own name from — so for any operation your spec gives an operationId, the migration does
not rename your surface on its own initiative. Renaming is available and deliberate, never something
done to you.
An operation with no operationId has no such name to carry, so Glotto builds one from the
route (get /pets/{petId} becomes get_pets_petId_) — and says so in the report, one line per
operation, because that is a name we chose and yours may differ.
It is not a guarantee that every member name matches, and the report is where you check. Your
gen.yaml may carry dated fixes flags and an sdkClassName, each of which moved names in the SDK
you have today; the conversion reports every one it finds, under exactly that heading, because those
are the reasons a migrated SDK’s public members could differ from the one it replaces. Pinning them
is a separate step, not part of this conversion.
If the document cannot be read, you get a clearly marked placeholder resource instead and the report
says which case you hit — a registry.speakeasyapi.dev reference, a URL (this command reads local
files and will not fetch one), a path that does not resolve, a document that will not parse, or one
that declares no operations. That one you do have to replace before you generate.
Overlays and merged documents are applied before the surface is read. Your workflow may stack
overlays or merge further documents on top of the document your source names, and an OpenAPI
Overlay can add or remove an operation. The conversion applies them first, in order and with the
same functions Glotto runs when it generates, so the resources block describes the surface your
SDK is generated from: an operation an overlay removes is not declared, and one an overlay adds
keeps its own name. The report says so under resources.
When a transform cannot be applied locally, you get a warning naming it and the reason, and the
block describes the primary document instead. That happens when an overlay is a URL (this command
reads local files and will not fetch one), a file is missing or will not parse, or the document is
Swagger 2.0 (Glotto converts it before applying anything). It also happens when an overlay action
matches nothing in your document. The Overlay standard skips such an action, but Glotto refuses it,
so fix or remove it before you generate. It also happens when an action’s JSONPath filter may read a
server URL or an example. Glotto normalizes those values before it applies overlays, so this command
cannot tell what the filter will select. Any other correction Glotto refuses, such as an invalid
JSONPath or a merged document that changes nothing, is reported the same way, with its reason. When
that verdict depends on a server URL or an example, the warning says it cannot be judged locally
instead of predicting what glotto generate will do.
A transform that does apply can still get a separate warning. That happens when an overlay action
or a merge anchor reaches an example, even through a wildcard, or a merged document writes a server
list or an example, or is anchored on a server. Your resources
block includes its effect, but glotto generate compares those normalized values and may refuse
it, so run it to confirm.
Next steps
Fill in the placeholders the report names, then:
glotto generate
From there every regeneration is checked rather than trusted: drift detection, compile and contract verification, and a verification report on each one. That is the part that was never really the generator’s job, and it is why the second push — and every push after it — is the thing Glotto is actually for.