feat: add aimlapi.com as an NLP service - #1
Open
Lookoff-AIMLAPI wants to merge 3 commits into
Open
Conversation
Parlant currently reaches multi-vendor catalogues either through a vendor-specific adapter or through LiteLLM. LiteLLM covers the routing but brings a heavy optional dependency, forces the local JinaAI fallback for embeddings on most setups, and hides per-model usage behind its own translation layer. aimlapi.com is an OpenAI-compatible gateway that also serves embeddings natively, so a first-class adapter gives Parlant a single key for 350+ chat models *and* a real remote embedder — with correct token accounting and no new dependency, since it reuses the `openai` client Parlant already ships. The one thing this adapter cannot copy from the other OpenAI-compatible services is how unset parameters are sent. aimlapi.com answers 400 when `temperature`, `top_p`, `seed`, `tools`, `tool_choice`, `response_format`, `stream`, `stream_options`, `parallel_tool_calls`, `max_tokens` or `max_completion_tokens` arrive as an explicit JSON null, while OpenAI accepts null for all of them. Forwarding a `None` hint — the obvious implementation — therefore fails on every real call while mocked tests stay green, so every request is built by omitting unset keys and three tests guard that invariant. Attribution headers follow the existing OpenRouter convention (HTTP-Referer / X-Title naming the calling application, here Parlant) plus the two headers the provider reads for partner attribution. They are built fresh per client and scoped to the api.aimlapi.com host, so they cannot ride along to another provider or to a proxy fronting the same API. Signed-off-by: aimlapi <opensource@aimlapi.com>
Moves aimlapi.com to the front of the three hand-ordered provider lists: the `NLPServiceName` literal, the `NLP_SERVICE_INITIALIZERS` map and the `parlant-server run` provider flags, plus the `NLPServices` factories in sdk.py. This is placement, not function — nothing here changes behaviour, and it is deliberately isolated in its own commit so it can be dropped before the change is offered upstream. Parlant has no "recommended"/featured badge mechanism for providers, so none was invented; the only lever available is list order. Signed-off-by: aimlapi <opensource@aimlapi.com>
The placeholder part_parlant was a readable stand-in chosen before the partner was registered. Registration mints the id server-side, so the real value is part_UOT3mCwOdpOUQKX2gIvYrCmv. A wrong or unknown partner id is accepted with a 200 and silently not attributed, so this would not have surfaced at runtime.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds aimlapi.com as a Parlant NLP service: a new adapter at
src/parlant/adapters/nlp/aimlapi_service.py, plus the usual three registry edits(
NLPServiceName,NLP_SERVICE_INITIALIZERS+--aimlapiflag inbin/server.py,NLPServices.aimlapiinsdk.py), a changelog line and a docs page. Same shape asthe merged Novita adapter (
novita_service.py), with the embedder and header handlingtaken from
openrouter_service.py.aimlapi.com is an OpenAI-compatible gateway over 353 chat models. It needs no new
dependency — it reuses the
openaiclient Parlant already ships — and unlike mostaggregators it serves embeddings natively (15 models), so
get_embedder()returns areal remote embedder instead of falling back to the local
JinaAIEmbedder.Why a first-class adapter and not "just use LiteLLM"
That is a fair question here, and the honest answer is a short list:
parlant[litellm]pulls inlitellm+torch+transformers. This adapter addsnothing to the dependency tree.
LiteLLMServicefalls back to local JinaAI embeddings unlessLITELLM_EMBEDDING_MODEL_NAMEis set; here the default embedder is a remoteopenai/text-embedding-3-large, 3072-dim, verified live.GenerationInfo.modelid come straight from the providerrather than through a translation layer.
The one thing that could not be copied from an existing adapter
aimlapi.com returns HTTP 400 when an optional field arrives as an explicit JSON
null— verified today againstPOST /v1/chat/completions:null(400)null(200)temperature,top_p,seed,tools,tool_choice,response_format,stream,stream_options,parallel_tool_calls,max_tokens,max_completion_tokensstop,presence_penalty,frequency_penalty,n,user,logprobs,logit_biasOpenAI accepts
nullfor all of them. So the natural implementation — forwarding anunset hint straight through, e.g.
temperature=hints.get("temperature")— produces anadapter that fails on every real call while its unit tests stay green, because the
tests mock the transport.
Every request in this adapter is therefore built through
omit_unset_arguments(), whichdrops keys whose value is
Nonerather than serialising them. Three tests guard it, andall three fail if the helper is reduced to
dict(arguments)(checked by mutating it):test_that_unset_arguments_are_omitted_rather_than_passed_as_nulltest_that_a_none_valued_hint_is_never_sent_to_the_apitest_that_no_none_valued_argument_is_sent_when_streamingThe 400 body names the offending field in
error.details[].path/.reason; thetop-level
messageis generic ("Validation failed. Please check your request fields…"),so
detailsis the thing to read when debugging.Attribution headers
Follows the convention already in
openrouter_service.py(HTTP-Referer/X-Titlenaming the calling application), plus the two headers aimlapi.com reads:
HTTP-RefererandX-Titleare overridable by the host application viaAIMLAPI_HTTP_REFERER/AIMLAPI_SITE_NAME, mirroringOPENROUTER_HTTP_REFERER/OPENROUTER_SITE_NAME. Properties covered by tests:MappingProxyTypeand is never mutated;api.aimlapi.com:build_attribution_headers()returns{}for any other host, so attribution cannot ride a request to another provider orto a proxy fronting the same API;
^part_[A-Za-z0-9]{1,64}$. A malformed id isignored by the gateway rather than rejected, so nothing at runtime would ever surface
a typo — the regex test is the only thing that catches it.
Models
Every id shipped here was checked against
GET /v1/models?include=all(936 rows, 785distinct ids, 353 with
type == "openai/chat-completions") as both anidand analias, and all four pre-configured chat models publishstructured_outputandtoolsin
capabilities:openai/gpt-4.1openai/gpt-4.1-minianthropic/claude-sonnet-4.5google/gemini-2.5-flashEmbedder default
openai/text-embedding-3-large(3072 dims, confirmed by a live call,not by the catalog). No model is removed or renamed by this PR.
Two deliberate omissions, both verified rather than assumed:
deepseek/deepseek-chatis not pre-configured even though it is a valid id: it ispublished both as its own model and as an alias of
deepseek/deepseek-v4-flash, and arequest for it is actually served by
deepseek-v4-flash. Anything that pins "the modelthat served the request" would see a mismatch.
anthropic/claude-sonnet-4.5). The dashedvariants of some Anthropic ids publish only
streamingincapabilitieseven thoughthe models are identical, so gating on capabilities with a dashed id is misleading.
Verification
Everything below was run on this branch. No
AIMLAPI_API_KEYvalue appears in anycommitted file, log or output.
Build / install —
uv sync --frozenclean;import parlant.bin.serverOK;parlant-server run --helplists--aimlapi.Tests —
pytest tests/adapters/nlp --continue-on-collection-errors:develop, pristine)The 3 collection errors are pre-existing and identical on a clean tree
(
test_azure_service.py,test_litellm_service.py,test_zhipu_service.py— theazure,litellmandzhipuoptional extras are not installed). The 20 new tests areall in
tests/adapters/nlp/test_aimlapi_service.py.Lint —
uv run python scripts/lint.py --mypy --ruff: 49 mypy errors in 23 filesbefore and after, i.e. zero new errors, while the checked file count goes from 289
to 291 (the two new files). All remaining errors are missing stubs for uninstalled
optional extras plus three pre-existing
entity_cq.pyerrors. The two new files areclean under
ruff checkandruff format. Notesrc/parlant/bin/server.pyand most ofadapters/nlp/*.pywere already non-conforming toruff formatbefore this branch;that was left alone rather than reformatted into the diff.
Live inference through the adapter (not curl, not a mock — via
AIMLAPIService.get_schematic_generator()/.get_embedder()/ streaming):The schematic call above was made with
hints={"temperature": None}on purpose: that isthe exact input that 400s if the parameter is forwarded instead of omitted.
Live multi-turn tool loop through the engine — a real
p.ServeronNLPServices.aimlapiwith a tool attached, two customer turns on one session:Turn 2 is the interesting one: a host that clears tools between turns by nulling the
field succeeds on turn 1 and 400s on turn 2. Parlant drives tools through schematic
generation rather than the native
toolsparameter, so it is not exposed to thatdirectly — but the loop was exercised end to end rather than assumed.
Commits
feat: add aimlapi.com as an NLP service— the adapter, registry wiring, tests, docsand changelog.
chore(aimlapi): fork-only placement — do not send upstream— moves aimlapi.com tothe front of the hand-ordered provider lists. Drop this commit before offering
anything upstream. It changes no behaviour. Parlant has no
"recommended"/featured badge concept for providers, so none was invented; list order
is the only lever.
Not verified
openai/gpt-4.1was exercised live; the other three pre-configured ids areverified against the catalog (
structured_output+tools+ context length) but werenot each driven through a full agent turn.
tests/core,tests/sdk,tests/api) was not run in full: itis an LLM-in-the-loop suite that CI runs against Emcie's own service, and running it
end to end is neither cheap nor deterministic here. The adapter subset plus the live
agent run above is what was verified.