Skip to content

Support aimlapi.com as an AI provider preset - #1

Open
Lookoff-AIMLAPI wants to merge 3 commits into
masterfrom
feat/aimlapi-provider
Open

Support aimlapi.com as an AI provider preset#1
Lookoff-AIMLAPI wants to merge 3 commits into
masterfrom
feat/aimlapi-provider

Conversation

@Lookoff-AIMLAPI

Copy link
Copy Markdown
Member

What

Adds aimlapi.com as an AI provider preset, and sends caller attribution headers when — and only when — a request actually goes to api.aimlapi.com.

Three commits, deliberately separable:

  1. :sparkles: Support aimlapi.com as an AI provider preset — one line in PROVIDER_PRESETS (app/src/config/tabs/ai/aiProviderUi.ts).
  2. :sparkles: Send caller attribution headers to aimlapi.comkernel/util/openai.go + a new kernel/util/openai_attribution_test.go.
  3. chore(aimlapi): fork-only placement — do not send upstream — moves the preset to the first position of the aggregator group. Commercial placement only. Drop this commit before any upstream proposal.

Why

AI/ML API is an OpenAI-compatible aggregator. A user can already reach it today by typing the base URL into a custom provider, so the preset is a discoverability change, not a capability one. It follows exactly the shape of the three aggregator presets already there (siliconflow, openrouter, groq).

No icon is added: groq ships without one, so the field is genuinely optional, and AGENTS.md §2.3 forbids hand-writing SVG.

No model ids are hardcoded. The provider's models come from its own GET /v1/models through the existing ListAvailableModels path, so the entry cannot go stale.

No new i18n keys: preset display names are literals in the preset table, not language keys, so langs/*.json is untouched and scripts/check-lang-keys.py is unaffected.

Attribution headers — how they are scoped

Injection happens in an openai.HTTPDoer wrapper (attributionTransport), the same client-level extension point the existing extraBodyTransport and the Gemini thought-signature transport already use. It is wired once in newProviderHTTPDoer(), so chat, streaming and the model list all get it without touching any call site.

The table is keyed by API host, not by configured provider. Headers are attached only if req.URL.Hostname() matches, so they cannot ride to another vendor, and cannot reach a third-party proxy that merely fronts the same API. AttributionHeadersForHost returns a copy, so a caller cannot mutate the shared table. Existing headers of the same name are preserved, never overwritten.

The headers carry no API key, no user content, and nothing identifying a user. HTTP-Referer and X-Title point at SiYuan, not at the vendor.

X-AIMLAPI-Partner-ID is asserted against ^part_[A-Za-z0-9]{1,64}$ in a test, because an invalid value is not rejected by the server — it is silently counted as untagged, so a typo would never surface at runtime.

Verification

Numbers are baseline (pristine master, 44a6c21) versus after.

Check Baseline After
go test ./util/ -count=1 -v (kernel) 272 run / 272 pass / 0 fail 278 run / 278 pass / 0 fail
node --import tsx --test src/config/tabs/ai/*.test.ts 9 pass / 0 fail 9 pass / 0 fail
pnpm run typecheck exit 0 exit 0
pnpm run lint (typecheck + eslint --fix) exit 0, no files rewritten
gofmt -l ./util/, go vet ./util/ clean

The six new tests are the attribution ones. Exit codes were read from the bare command, not through a pipe.

pnpm build was not run — AGENTS.md §1 forbids it. The kernel binary was not compiled — AGENTS.md §1.3 forbids that too; the touched package was type-checked and tested instead.

One real call through the code path

Driven through util.NewOpenAIClientWithModel + util.CreateOpenAICompletion — the same functions kernel/model/ai.go and kernel/api/agent.go call — against https://api.aimlapi.com/v1, model openai/gpt-4o-mini, with a real key supplied via the environment. Headers were captured with httptrace.ClientTrace.WroteHeaderField, i.e. as actually written on the wire, not as configured.

CHAT {"id":"chatcmpl-EJrdoM83fMdsfNNBsm73cFGfP1bqQ","model":"gpt-4o-mini-2024-07-18",
      "content":"SIYUAN-AIMLAPI-OK","finish_reason":"stop",
      "usage":{"prompt_tokens":20,"completion_tokens":9,"total_tokens":29}}

HEADERS-ON-WIRE {"authorization":"Bearer <redacted>",
                 "http-referer":"https://github.com/siyuan-note/siyuan",
                 "user-agent":"SiYuan/0.0.0",
                 "x-aimlapi-partner-id":"part_siyuan",
                 "x-aimlapi-source":"agent/siyuan",
                 "x-title":"SiYuan"}

TOOLCALL {"model":"gpt-4o-mini-2024-07-18","finish_reason":"tool_calls",
          "tool_calls":[{"id":"call_hELM8UkZ7a9mHNObdZNqFkLu","type":"function",
                         "function":{"name":"get_weather","arguments":"{\"city\":\"Kunming\"}"}}]}

TESTMODEL count=936 matched=true err=<nil>

Tool calling works on the same path, so the agent runtime is covered too. The probe file was temporary and is not part of this branch.

A known trap for OpenAI-compatible providers is a client that serialises unset optional parameters as literal null; several models reject tools: null with a 400 on turn 2 of an agent loop while turn 1 succeeds. This was checked rather than assumed: in sashabaranov/go-openai, Tools, ToolChoice, ResponseFormat, Seed, Temperature, TopP, MaxTokens, MaxCompletionTokens, StreamOptions and ParallelToolCalls are all omitempty, and Stream is a plain bool that serialises as false. Unset keys are omitted, so this codebase is structurally immune.

Not fixed here, but found while verifying

util.TestModel validates a provider by calling ListModels first and only falls back to a completion request if that fails. Against this provider, GET /v1/models returns 200 for any key, including a garbage one. Running the same code path with sk-not-a-real-key-000 returned matched=true, err=<nil> — so the provider "test" action reports success for an invalid key, and the real failure only appears later at the first chat request, where it reads as a broken model rather than a bad key.

That is a provider-side quirk, and fixing it here would change behaviour for every provider, so it is out of scope for this PR. It is worth a separate issue.

Relatedly, that call returns 936 entries with no type filter, of which only 353 are chat models, and 91 ids appear twice under different endpoint types (151 duplicate rows). The model dropdown therefore shows non-chat models and repeats some ids. Also out of scope here.

What could not be verified

  • The kernel binary was not built and no running kernel was exercised, per AGENTS.md §1.3. Verification was the util package's own tests plus the live call above.
  • The provider catalog dialog was not exercised in a browser; pnpm dev/pnpm build are forbidden by AGENTS.md §1. The change there is one entry in a typed literal array and is covered by tsc.

AI/ML API is an OpenAI-compatible aggregator, so it already works today by
typing its base URL into a custom provider. The preset removes that manual
step and makes the endpoint discoverable in the provider catalog, the same
way SiliconFlow, OpenRouter and Groq are.

The entry carries no icon on purpose: Groq has none either, and hand-writing
an SVG is not allowed. No model list is hardcoded — the models are read from
the provider's own /v1/models, so the entry cannot go stale.
Providers that run an aggregator want to know which application traffic came
from, and aimlapi.com reads four headers to do it. Without them SiYuan's
requests are indistinguishable from anonymous ones.

The headers are keyed by API host rather than by configured provider, so they
cannot ride along to another vendor or to a third-party proxy that merely
fronts the same API. They carry no API key, no user content and nothing that
identifies a user. Existing headers are never overwritten, and the lookup
returns a copy so a caller cannot mutate the shared table.

Injection sits in a HTTPDoer wrapper, the same client-level extension point
the extra-body and Gemini thought-signature adapters already use, so it
applies to chat, streaming and the model list without touching call sites.

The partner id is covered by a regex test: an invalid value is not rejected
by the server, it is silently counted as untagged, so a typo would otherwise
never surface.
Moves the aimlapi.com preset to the first position of the aggregator group so
it is the first card a user sees in that section of the provider catalog.

This is a commercial placement preference, not a functional change, and it is
deliberately isolated in one commit so it can be dropped before any upstream
proposal. The category grouping and category order are left untouched: the
catalog renders official, aggregator, local, custom in that fixed order, and
reordering those would change unrelated UI.

The repository has no "recommended" or "featured" badge concept anywhere —
the generic b3-chip component is only used for user titles, bazaar
deprecation and error states, keyword tags, spellcheck languages and font
selection — so no badge is added.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant