Protocol compatibility
How the wire protocol evolves without breaking deployed apps — additive changes, SDK identity, and what each side tolerates.
The wire protocol between an SDK and the Cosmo backend carries no version number. It stays compatible a different way: the protocol only ever changes additively, each side tolerates what it doesn't recognize, and every session announces which SDK opened it — so the server always knows what is deployed in the field.
Additive evolution
A protocol change adds; it doesn't remove or retype. Concretely:
- A new field arrives as an optional field with a safe default — a client that doesn't send it and a client that doesn't read it both keep working.
- A new message type may appear on the stream at any time. SDKs surface an unrecognized
typeas an explicit unknown event and keep the session alive — a decode failure is never terminal. - A new value may appear in an enum the server authors — the error code, the session and usage statuses, the credential kind. Each SDK accepts one it does not name and hands the raw value through, so a new value costs you a
defaultbranch and nothing else; without that tolerance it would cost the whole payload, since an unrecognized code fails the event carrying it. Python hands back a member of the enum either way — a known value is the member you expect, an unrecognized one a member carrying the raw string, soisinstancedoes not separate them andvalue in list(TheEnum)does. TypeScript widens the type, and Swift carries it asunknown(String). Switch on the values you know and handle the rest generically. The enums the SDK itself raises — the*ErrorCodeon a thrown error — stay closed and exhaustive. - The server is strict about inbound keys: an unknown field on a client message is a schema error, never quietly accepted — so a request that would otherwise lose meaning (a mistyped option, a feature this backend doesn't have yet) doesn't half-apply. On session start you see it directly: a
422naming the offending field. On the data channel the frame is refused and dropped, and the rejection is recorded server-side rather than sent back, because anerrorframe there would read as terminal to current clients and end the call;invalid_messageis reserved for it. Tolerance is the client's job, not the server's.
An app built against a supported SDK keeps connecting as the backend evolves, and a wire change alone never forces an SDK upgrade. (One key the current Python SDK stamps and the wire ignores — a local message id — is accepted as a declared legacy field until that release leaves support; it is absent from the published schema.)
SDK identity
Every session-config carries an sdk block identifying the client:
{
"type": "session-config",
"sdk": { "name": "cosmo-ai-sdk", "version": "X.Y.Z" },
"agent": { "type": "inline", "instructions": "Be concise." }
}Every session-config must carry it: an identity is part of the contract, not an optional extra, so telemetry has no anonymous bucket and a deprecation decision can see the whole fleet. name is the SDK's package name in its registry — cosmo-ai-sdk (PyPI), cosmo-ai (npm), cosmo-swift-sdk (SwiftPM); version is the installed package version. Together the pair is exactly what is installed. The SDKs stamp this automatically — there is nothing to configure.
The same identity also travels as an X-Cosmo-SDK: <name>/<version> header on every REST call the SDK makes to Cosmo, so even a request refused before its body is parsed — a bad credential, a config the server rejects — is attributable to an SDK version in the server's logs. Calls to your infrastructure (a TokenSource endpoint on your backend) carry no Cosmo headers.
The identity is a claim, not a credential. The server groups telemetry by it and records it per session, which is what lets a deprecation decision be grounded in which SDK versions are actually live — and if a version of an SDK ever stops being supported, the refusal can name the package and the version to upgrade to, rather than pointing at an opaque protocol number. An identity the server doesn't recognize (your own client sending the header, a fork) is simply bucketed as-is; it is never itself a reason to refuse a request. Deployed apps keep sending the name they were built with, so if a package is ever renamed the server maps the old name onto the new release line.
The SDK version at runtime
import { SDK_NAME, SDK_VERSION } from 'cosmo-ai';
console.log(`${SDK_NAME} SDK ${SDK_VERSION}`);from cosmo_ai import SDK_NAME, SDK_VERSION
print(f"{SDK_NAME} SDK {SDK_VERSION}")These are the exact values the SDK stamps on session-config.sdk and the X-Cosmo-SDK header. The version travels inside the package, so it reads the same whether the package was installed from PyPI, vendored into your tree, or frozen into a bundle.
import CosmoRealtime
print("\(sdkName) SDK \(sdkVersion)")When a breaking change does happen
Before 1.0, a breaking wire change ships as a coordinated SDK release: the SDKs move together, and the changelog's Breaking entries name the migration. The version_mismatch error code remains reserved on the wire — current servers don't send it, and each SDK still maps it to its typed session-start error if an older server refuses the connection with it.
Package versioning itself — what a major, minor, or patch means for each SDK — is a separate concern, covered in Versioning.