Model & storage providers (declarative adapters)
These three contribution points let an extension register a provider that Glixo's host already knows how to drive (a built-in LLM or storage backend), or — for a wire protocol the host doesn't have a backend for yet — spawn your own out-of-process adapter. All three are declared under contributes.* in glixo.module.json; none require you to ship a UI.
LLM provider (contributes.llmProviders)
What it is + when to use it. Registers a selectable LLM provider in the model picker/settings — credential fields, model discovery, and model-name routing — without writing any inference code. Use this when the provider speaks a wire protocol the host already implements (openai-compatible or anthropic); the host's built-in adapter does the actual chat call.
Manifest fragment (from glixo-community-modules/catalog/code/cliproxy-provider/glixo.module.json, a real installable community provider):
{
"id": "glixo.providers.cliproxy",
"name": "CLIProxy LLM Provider",
"publisher": "Glixo",
"kind": "extension",
"version": "0.1.0-alpha",
"protocolVersion": "0",
"runtime": { "kind": "node", "version": "20" },
"entry": { "command": "echo", "args": ["declarative-llm-provider"] },
"capabilities": ["glixo.llm.provider.cliproxy"],
"requires": ["capability.glixo.llmProviders.register"],
"contributes": {
"llmProviders": [
{
"id": "cliproxy",
"label": "CLIProxy",
"description": "Use a self-hosted or tunneled CLIProxy gateway.",
"icon": "git-network-outline",
"contractId": "glixo.adapter.llm.v1",
"implementationId": "openai-compatible",
"credentialFields": [
{ "id": "baseUrl", "label": "Base URL", "type": "url", "required": true, "defaultValue": "http://127.0.0.1:8317/v1" },
{ "id": "apiKey", "label": "Bearer token", "type": "secret", "required": false, "secret": true }
],
"modelDiscovery": { "kind": "openai-compatible-models", "path": "/models" },
"modelRouting": {
"families": ["openai", "anthropic", "cliproxy"],
"patterns": ["*"],
"servesAllFamilies": true
},
"capabilities": ["chat", "streaming", "tools", "images", "reasoning"]
}
]
}
}
Every field name here is enforced by the reader (LlmProviderContributionReader.Read, glixo-platform/src/Glixo.ServiceCatalog/LlmProviderContributions.cs:142-196), which silently drops an entry missing id/label/contractId/implementationId. Enum-constrained fields:
contractId: only"glixo.adapter.llm.v1"(LlmProviderContributions.cs:11-14).implementationId: pick a built-in kind to piggyback on the host's chat client —"openai-compatible" | "openai" | "openai-oauth" | "anthropic"(LlmProviderContributions.cs:17-23; seeglixo-community-modules/bundled/anthropic/glixo.module.jsonusing"anthropic", andglixo-community-modules/bundled/ollama/glixo.module.json/glixo-community-modules/bundled/openai/glixo.module.jsonboth using"openai-compatible"). A customimplementationIdinstead pairs with your owncontributes.adapters[]entry (see the Adapter section below) — and cannot collide with these four built-ins (ModuleManifestValidator.cs:519-529, backstopped again at runtime inExtensionChatAdapterRegistrar.cs:74-85).credentialFields[].type:"url" | "secret" | "text" | "oauth".modelDiscovery.kind: only"openai-compatible-models"or"anthropic-models"— omitmodelDiscoveryentirely for a static model list (ship a top-level"models": [...]array instead, asglixo.providers.openai-oauthdoes).capabilities[]:"chat" | "streaming" | "tools" | "images" | "reasoning"(LlmProviderContributions.cs:33-40).
Declaring this block requires "capability.glixo.llmProviders.register" in requires[] (LlmProviderContributionContract.RegisterCapability, LlmProviderContributions.cs:8; enforced ModuleManifestValidator.cs:661-666).
The code you write. None, for a built-in implementationId — this is why entry.command is a placeholder ("echo" / "declarative-llm-provider", per every provider manifest above including the bundled anthropic/ollama/openai ones): the host never spawns it. It resolves your implementationId straight to an in-process IChatClient (anthropic → AnthropicChatClient, openai-compatible → CliProxyChatClient, which is what actually backs both ollama and cliproxy).
How the host consumes it. Built-in kinds are wired once at startup by ChatClientFactory.RegisterBuiltInChatAdapters (glixo-platform/src/Glixo.CodeAgent/Runtime/ChatClientFactory.cs:27-34); which provider serves a given model string is decided by BundledLocalDataStore.ProviderServesModel (glixo-platform/src/Glixo.Code.Server/BundledLocalDataStore.cs:554-586) using your modelRouting block.
Sample to study. glixo-community-modules/catalog/code/cliproxy-provider/glixo.module.json (installable, servesAllFamilies: true gateway); also glixo-community-modules/bundled/anthropic/glixo.module.json, glixo-community-modules/bundled/ollama/glixo.module.json, glixo-community-modules/bundled/openai/glixo.module.json for the three built-in implementationId shapes.
Storage provider (contributes.storageProviders)
What it is + when to use it. Registers a selectable cloud storage backend (for Glixo's file replication and short-lived attachment download links) with OAuth wiring, without writing any storage code. Use this when the backend is one of the host's built-in storage adapters (onedrive, dropbox).
Manifest fragment (from glixo-community-modules/catalog/code/onedrive-storage/glixo.module.json):
{
"id": "glixo.storage.onedrive",
"name": "OneDrive Storage Provider",
"publisher": "Glixo Community",
"kind": "extension",
"version": "0.1.0-alpha",
"protocolVersion": "0",
"runtime": { "kind": "node", "version": "20" },
"entry": { "command": "echo", "args": ["declarative-storage-provider"] },
"capabilities": ["glixo.storage.provider.onedrive"],
"requires": ["capability.glixo.storage.providers.register"],
"permissions": ["network.microsoft-graph"],
"contributes": {
"storageProviders": [
{
"id": "onedrive",
"label": "OneDrive",
"description": "Use Microsoft OneDrive for Glixo replication and short-lived raw download links.",
"icon": "cloud-outline",
"contractId": "glixo.adapter.storage.v1",
"implementationId": "onedrive",
"credentialFields": [
{ "id": "oauth", "label": "Microsoft account", "type": "oauth", "required": true, "secret": true,
"helpText": "Core opens the Microsoft sign-in callback and stores the token bundle." },
{ "id": "clientId", "label": "Client ID override", "type": "text", "required": false,
"helpText": "Optional advanced override for a user-owned Microsoft app registration." }
],
"oauth": {
"authorizationUrl": "https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize",
"tokenUrl": "https://login.microsoftonline.com/consumers/oauth2/v2.0/token",
"scopes": ["offline_access", "User.Read", "Files.ReadWrite"],
"redirectPath": "/v1/code/storage/oauth/callback",
"pkce": true
},
"capabilities": ["replication", "publicUrl", "cloud", "highAvailability"]
}
]
}
}
Read by StorageProviderContributionReader.Read (glixo-platform/src/Glixo.ServiceCatalog/StorageProviderContributions.cs:130-182), same required-field gate as LLM providers (id, label, contractId, implementationId). Enum-constrained fields:
contractId: only"glixo.adapter.storage.v1"(StorageProviderContributions.cs:11-14).implementationIdbuilt-ins:"onedrive" | "dropbox" | "storage-http"(StorageProviderContributions.cs:17-22) — notestorage-httpvalidates but has no registered backend yet (CloudStorageProviderBackends.cs:14-15deliberately skips it).credentialFields[].type: same four values,"url" | "secret" | "text" | "oauth".capabilities[]:"replication" | "publicUrl" | "cloud" | "highAvailability"(StorageProviderContributions.cs:32-38).oauth.redirectPathdefaults to"/v1/code/storage/oauth/callback"andoauth.pkcedefaults totrueif omitted.
Declaring this block requires "capability.glixo.storage.providers.register" (StorageProviderContributionContract.RegisterCapability, StorageProviderContributions.cs:8). Note the companion permissions entry above ("network.microsoft-graph" / "network.dropbox" in the Dropbox sibling manifest) — that's a separate network-egress permission, not part of the contribution schema itself.
The code you write. None — as both sample READMEs state verbatim: "Core owns the OAuth callback, account storage, attachment routing, and replication runtime. This community module contributes metadata, scopes, and capability flags only" (glixo-community-modules/catalog/code/onedrive-storage/README.md, .../dropbox-storage/README.md). entry.command is the same "echo" placeholder — never spawned.
How the host consumes it. Built-in storage kinds are registered once into StorageAdapterRegistry by CloudStorageProviderBackendFactory.RegisterBuiltInStorageAdapters (glixo-platform/src/Glixo.Code.Server/CloudStorageProviderBackends.cs:17-30); the settings UI lists installed providers via SettingsStorageProviders.tsx:604-605 (glixo-code/sources/components/storage/SettingsStorageProviders.tsx), reading contributes.storageProviders off each installed module.
Sample to study. glixo-community-modules/catalog/code/onedrive-storage/glixo.module.json and glixo-community-modules/catalog/code/dropbox-storage/glixo.module.json — both installable, both OAuth+PKCE, both built-in implementationIds.
Generic out-of-process adapter (contributes.adapters)
What it is + when to use it. Pairs a custom implementationId (one not in the host's built-in list) with an actual spawned process that speaks the adapter wire protocol over stdio. Use this only when no built-in adapter fits — e.g. a wire protocol the host doesn't natively implement. Today only the LLM chat contract (glixo.adapter.llm.v1) is host-supported for this path; there is no generic storage out-of-process adapter path shipped yet.
Manifest fragment (from the working tutorial at glixo-extensions-docs/samples/providers/adapter/glixo.module.json; developer examples are deliberately separate from marketplace products):
{
"id": "glixo.providers.reference-echo",
"name": "Reference Echo LLM Adapter",
"publisher": "Glixo",
"kind": "extension",
"version": "0.1.0-alpha",
"protocolVersion": "0",
"runtime": { "kind": "node", "version": "20" },
"entry": { "command": "echo", "args": ["out-of-process-llm-adapter"] },
"capabilities": ["glixo.llm.provider.reference-echo"],
"requires": [
"capability.glixo.llmProviders.register",
"capability.glixo.agent.adapters.register"
],
"permissions": [],
"contributes": {
"llmProviders": [
{
"id": "reference-echo",
"label": "Reference echo (out-of-process)",
"description": "Reference out-of-process adapter — echoes your last message; no key required.",
"icon": "terminal-outline",
"contractId": "glixo.adapter.llm.v1",
"implementationId": "reference-echo",
"credentialFields": [],
"modelRouting": {
"families": ["reference"],
"patterns": ["echo*", "reference*"],
"servesAllFamilies": false
},
"capabilities": ["chat", "streaming"]
}
],
"adapters": [
{
"contractId": "glixo.adapter.llm.v1",
"implementationId": "reference-echo",
"transport": "stdio",
"command": "node",
"args": ["adapter.js"]
}
]
}
}
Note the requires[] list carries two capabilities — capability.glixo.llmProviders.register for the llmProviders[] block plus a distinct adapters capability (checked by ModuleManifestValidator.ValidateAdapters, glixo-platform/src/Glixo.ServiceCatalog/ModuleManifestValidator.cs:456-480) for the adapters[] block. The schema requires contractId, implementationId, command on each adapter entry (glixo-platform/catalog/schema/glixo.module.schema.json:492); transport must be "stdio" — it's the only transport the host supports today (ModuleManifestValidator.cs:538-544). An implementationId that collides with a built-in (openai-compatible/openai/openai-oauth/anthropic) is rejected at validation time.
The code you write. A long-lived process (here node adapter.js, cwd = the extension's install dir) that reads newline-delimited JSON requests from stdin and writes newline-delimited JSON responses to stdout. The working Textstats example is under glixo-extensions-docs/samples/providers/adapter/ and includes its own CommonJS package.json, so it runs independently of ancestor module settings:
host -> adapter (one line):
{ "id": "<corr>", "model": "<id>",
"messages": [ { "role": "system|user|assistant|tool", "content": "..." } ],
"options": { "temperature"?: number, "maxOutputTokens"?: number } }
adapter -> host (newline-delimited, correlated by "id"):
{ "id": "<corr>", "delta": "<text>" } // zero or more
{ "id": "<corr>", "done": true, "finishReason": "stop",
"usage": { "inputTokens": n, "outputTokens": n } } // exactly one terminal
— or —
{ "id": "<corr>", "error": "<message>" }
The host never puts secrets on the wire — it injects the resolved account into your process's environment as process.env.GLIXO_ADAPTER_API_KEY, GLIXO_ADAPTER_BASE_URL, GLIXO_ADAPTER_MODEL (adapter.js:23-30), which the echo reference doesn't need but a real provider would read.
How the host consumes it. ExtensionChatAdapterRegistrar.Register (glixo-platform/src/Glixo.Daemon/Sessions/ExtensionChatAdapterRegistrar.cs:37, 153 lines total) registers your adapters[] entry into a session-scoped AdapterRegistry.SessionAdapterScope (never the shared/global registry) keyed by implementationId; when a session's resolved LLM credential names that same kind, AdapterRegistry.CreateChatClient spawns your declared command with the extension's install dir as cwd and drives it over stdio — visible only to that session, torn down at session teardown.
Sample to study. glixo-extensions-docs/samples/providers/adapter/ (glixo.module.json, adapter.js, and package.json). The old Echo module is retained only under glixo-community-modules/examples/ as a minimal wire-protocol illustration and is not a marketplace listing. There is no storage-side process-adapter equivalent shipped today.