We chose SDK calls that feel natural to read, and the result is that the client can fade into an engineer's code. An awkward client biases developers against the platform even when its service meets their needs. A natural one becomes part of the code without demanding attention.
We've released two versions of the Monogram Python client and one TypeScript version. Some choices from Python's first release still look right; others do not. This is a practical account of those decisions, their reasons, and what we've revised.
Usability over brevity
Our first major choice was favoring explicit, readable calls over short, clever ones. We use keyword arguments heavily, choose config objects over positional parameter lists, and keep names consistent even when a shorter isolated name might read well.
The alternative is a fluent API whose chained methods create compact code. It is attractive: client.prompt("summarize").run(text) looks good in a blog post. Yet intermediate results are difficult to inspect, since client.prompt("summarize") is unclear, and chaining couples operations, complicating logs and debugging. Failures deep in the chain can also be confusing.
For developer tooling, we think legibility matters more than cleverness. Monogram users build serious production features and need code they can understand in a pull request six months later. That favors explicit, documented calls that are easy to inspect.
We are not calling fluent APIs generally wrong. They suit some domains. But eval and deployment tooling often involves subtle behavioral differences, so we prefer explicit calls there.
The async choice, and what we'd change
This is our clearest change.
When we built the Python client, we made async the primary interface because most production LLM applications use async Python. That was right. Applications built with fastapi or asyncio expect async client methods, while sync-only methods would hinder production use.
Our mistake was treating sync methods as secondary. They were absent for some operations in the early client and hard to find in the docs. Developers running eval scripts or exploring the API in a notebook quickly encountered asyncio.run() requirements. Experienced Python developers can handle that, but it adds friction for others, especially newer-to-Python engineers writing eval scripts.
In Q3, we added sync wrappers for every primary operation, with async versions exposed through consistently named async counterparts. Both are first-class. The pattern is:
client.evals.run(eval_id="my-eval")
await client.evals.run_async(eval_id="my-eval")
This was not groundbreaking, but it removed recurring friction. We should have included it initially. "Most production use cases" does not mean "most developers who first try the SDK." Exploration and scripting are often synchronous, and new users commonly follow that path before production integration.
Errors that explain what failed
SDK error design gets too little credit. Error messages determine the debugging experience, and in developer tooling they are part of the product more than they are for, say, a payment processor.
Our rule is that errors should explain what failed and where to investigate, not merely report failure. A PromptNotFoundError should include the supplied prompt identifier and say whether a slug mismatch or deployment environment issue may be responsible. An EvalSchemaValidationError should identify the failed field and expected type.
This seems obvious, but it takes deliberate work. The easiest route is catching exceptions from HTTP calls and re-raising a generic message. That saves ten minutes writing an error and costs users ten minutes in each debugging session.
We log structured context objects with exception messages. When a request fails, the client records the request ID, prompt version, and eval run ID active then. This makes a particular failure quick to find in production logs, even when surrounding application code did not capture those identifiers.
TypeScript client design: lessons from Python
We released the TypeScript client about four months after Python's, so we could use what we had learned.
TypeScript's type system offers an advantage Python lacks to the same degree. We used it fully: every response is typed, every config object has a typed interface, and common errors use discriminated unions instead of exception hierarchies. TypeScript users get autocompletion and compile-time checks that catch some integration bugs before execution.
TypeScript's async design was easier from the beginning because TypeScript/JavaScript is async-native in a way Python is not. Promises are expected, so Python's sync/async split created less comparable friction. We supported clean one-liner usage, without maintaining a parallel sync interface.
Versioning and breaking updates
Our versioning rule is simple: minor versions remain backward-compatible, while major versions do not. We have not released a major version, but in about eight months we released eleven Python client minor versions. Every release has a changelog and a clear migration note for behavior changes, including non-breaking ones.
The practice that has spared us the most pain is marking deprecated interfaces with a deprecation warning at least two minor versions before removal. Users on older client versions see the warning in logs, can update in time, and are not surprised when the interface vanishes. This is standard in theory, but many early-stage developer tool SDKs omit it while moving fast. Surprises cost more in developer tooling because users lack the business reason to trust you that they may have with a payment processor or cloud provider. Predictable, clearly communicated changes build trust; silent deprecations weaken it.
What remains wrong
Configuration loading is more confusing than we want. We support environment variables, a config file, and explicit constructor arguments, but their precedence is not clear enough. Users have asked about it repeatedly. The API works, yet understanding it takes too much mental effort. A proper fix requires a breaking change, so we postponed it to the next major version.
The TypeScript client also trails Python in documentation coverage. Reference pages exist, but examples vary in quality. Feature work keeps pushing this backlog item back. We know it needs attention.
An SDK requires ongoing commitment, not a project with an end date. We will maintain, extend, and revise today's release for the product's lifetime. Good foundational choices matter, as does revisiting the ones that prove wrong.