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:

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 (anthropicAnthropicChatClient, openai-compatibleCliProxyChatClient, 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:

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.