Skip to content

Comments

feat: add native Mistral AI provider support#541

Open
edouard-claude wants to merge 2 commits intosipeed:mainfrom
edouard-claude:feat/mistral-provider
Open

feat: add native Mistral AI provider support#541
edouard-claude wants to merge 2 commits intosipeed:mainfrom
edouard-claude:feat/mistral-provider

Conversation

@edouard-claude
Copy link
Contributor

@edouard-claude edouard-claude commented Feb 20, 2026

Summary

  • Adds Mistral AI as a first-class provider (18th provider) alongside existing ones
  • Uses OpenAI-compatible API at https://api.mistral.ai/v1 with provider-specific model prefix stripping
  • Supports both new model_list format and legacy providers config migration
  • Fixes tool calling compatibility: removes extra name/arguments fields from tool_calls JSON serialization that Mistral's strict API rejects (OpenAI silently ignores them)
  • References: closes the gap identified in feat: Add Mistral AI provider support #246, builds on the approach from feat: add Mistral AI provider, Firecrawl and SerpAPI tools #193

Changes

  • pkg/config/config.go: Add Mistral ProviderConfig to ProvidersConfig, IsEmpty(), HasProvidersConfig()
  • pkg/config/defaults.go: Add mistral-small entry in default model_list
  • pkg/config/migration.go: Add mistral migration entry for ConvertProvidersToModelList()
  • pkg/config/migration_test.go: Update AllProviders test (17 → 18)
  • pkg/providers/factory_provider.go: Add "mistral" to OpenAI-compat protocol list + getDefaultAPIBase()
  • pkg/providers/factory.go: Add "mistral" case in legacy resolveProviderSelection()
  • pkg/providers/openai_compat/provider.go: Add "mistral" to prefix strip list
  • pkg/providers/protocoltypes/types.go: Fix ToolCall.Name and ToolCall.Arguments serialization (json:"-") — these internal fields were incorrectly included in API payloads, causing 422 errors on strict APIs like Mistral
  • pkg/migrate/config.go: Add "mistral" to supportedProviders
  • config/config.example.json: Add mistral config section

Configuration

{
  "model_list": [
    {
      "model_name": "mistral-small",
      "model": "mistral/mistral-small-latest",
      "api_key": "YOUR_API_KEY",
      "api_base": "https://api.mistral.ai/v1"
    }
  ]
}

E2E Test Logs

Simple chat (model=mistral-small-latest):

[INFO] agent: Registered agent {agent_id=main, name=Main Agent, model=mistral-small-latest}
[INFO] agent: LLM response without tool calls (direct answer) {iteration=1, content_chars=40}
[INFO] agent: Response: I'm picoclaw, your helpful AI assistant.

Tool call — read_file:

[INFO] agent: LLM requested tool calls {tools=[read_file], count=1, iteration=1}
[INFO] agent: Tool call: read_file({"path":"/Users/edouard/.picoclaw/workspace/test.txt"})
[INFO] tool: Tool execution completed {tool=read_file, duration_ms=0, result_length=23}
[INFO] agent: LLM response without tool calls (direct answer) {iteration=2, content_chars=61}
[INFO] agent: Response: The file test.txt contains the text: "test file for picoclaw"

Tool call — list_dir:

[INFO] agent: LLM requested tool calls {tools=[list_dir], count=1, iteration=1}
[INFO] agent: Tool call: list_dir({"path":"/Users/edouard/.picoclaw/workspace"})
[INFO] tool: Tool execution completed {tool=list_dir, duration_ms=0, result_length=146}
[INFO] agent: Response: Here are the files and directories in my workspace: AGENT.md, IDENTITY.md, SOUL.md, USER.md, aieos.json, test.txt, memory/, sessions/, skills/, state/

Tool call — exec (shell):

[INFO] agent: LLM requested tool calls {tools=[exec], count=1, iteration=1}
[INFO] agent: Tool call: exec({"command":"echo hello from mistral"})
[INFO] tool: Tool execution completed {tool=exec, duration_ms=6, result_length=19}
[INFO] agent: Response: The output of the command is: hello from mistral

Test plan

  • All existing tests pass (go test ./... — 0 failures)
  • New migration test passes (18 providers)
  • gofmt, go vet clean
  • E2E simple chat with mistral-small-latest
  • E2E tool calling: read_file, list_dir, exec — all working
  • No breaking changes to existing providers (extra fields were silently ignored by OpenAI)

@harshbansal7
Copy link
Contributor

harshbansal7 commented Feb 20, 2026

End-to-end tested with mistral-small-latest model via picoclaw agent

@edouard-claude Can you add logs that were generated during that chat in the description just to be sure?

@lukemilby
Copy link
Contributor

@edouard-claude does tool calling work with the HTTP provider? Following @harshbansal7 comment. Would like to see that or create a test ensuring tool calling work.

@edouard-claude
Copy link
Contributor Author

Thanks @harshbansal7 @lukemilby for the great feedback! Your comments actually led me to discover a tool calling compatibility issue — Mistral's API is stricter than OpenAI's and rejects extra fields (name, arguments) at the top level of tool_calls in assistant messages. I pushed a fix that cleans up the JSON serialization in protocoltypes/types.go (fields are still available internally, just not serialized to the API payload).

Here are the E2E logs with tool calling working:

Simple chat:

[INFO] agent: LLM response without tool calls (direct answer) {agent_id=main, iteration=1, content_chars=40}
[INFO] agent: Response: I'm picoclaw, your helpful AI assistant.

Tool call — read_file:

[INFO] agent: LLM requested tool calls {agent_id=main, tools=[read_file], count=1, iteration=1}
[INFO] agent: Tool call: read_file({"path":"/Users/edouard/.picoclaw/workspace/test.txt"})
[INFO] tool: Tool execution completed {tool=read_file, duration_ms=0, result_length=23}
[INFO] agent: Response: The file test.txt contains the text: "test file for picoclaw"

Tool call — list_dir:

[INFO] agent: LLM requested tool calls {agent_id=main, tools=[list_dir], count=1, iteration=1}
[INFO] agent: Tool call: list_dir({"path":"/Users/edouard/.picoclaw/workspace"})
[INFO] tool: Tool execution completed {tool=list_dir, duration_ms=0, result_length=146}
[INFO] agent: Response: Here are the files and directories in my workspace: AGENT.md, IDENTITY.md, SOUL.md...

Tool call — exec (shell):

[INFO] agent: LLM requested tool calls {agent_id=main, tools=[exec], count=1, iteration=1}
[INFO] agent: Tool call: exec({"command":"echo hello from mistral"})
[INFO] tool: Tool execution completed {tool=exec, duration_ms=6, result_length=19}
[INFO] agent: Response: The output of the command is: hello from mistral

All 3 tool types (read_file, list_dir, exec) work correctly with mistral-small-latest. I'll update the PR description with these logs too.

@edouard-claude edouard-claude force-pushed the feat/mistral-provider branch 2 times, most recently from 96b1b63 to 5d7a85a Compare February 21, 2026 01:46
Name string `json:"name,omitempty"`
Arguments map[string]any `json:"arguments,omitempty"`
Name string `json:"-"`
Arguments map[string]any `json:"-"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change hides Name and Arguments from JSON serialization, which could be a breaking change for downstream consumers. Could you share the reasoning behind this decision and whether it’s been tested with other setups outside Mistral?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question! These two fields (Name and Arguments) are duplicates of Function.Name and Function.Arguments — they were populated internally after JSON parsing (see the deserialization logic in the provider code) but were never meant to be serialized back to the API.

The OpenAI spec only expects tool_calls[].function.name and tool_calls[].function.arguments — the top-level name/arguments fields don't exist in the spec. OpenAI silently ignores them, but Mistral's stricter validation returns a 422 error.

Since the data already lives in Function.Name/Function.Arguments (which remain serialized normally), removing the duplicates from JSON output doesn't change any behavior — it just stops sending fields that shouldn't be there.

Tested with: Mistral (mistral-small-latest) including tool calling (read_file, list_dir, exec). Other OpenAI-compatible providers are unaffected since they already ignore these extra fields.

Copy link
Contributor

@PixelTux PixelTux left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the clarification—everything looks good now. LGTM!

@vietvudanh
Copy link

Looks great, I was testing with new model_list and have 422 too on tools as Mistral is not following strictly with OpenAI specs. I was about to code then meet this PR, looks good.

Copy link
Contributor

@lukemilby lukemilby left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Looks good

Copy link
Collaborator

@alexhoshina alexhoshina left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fix is already in main, perhaps we can keep the commits clean by rebasing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! Rebased on main and dropped the shell_test.go commit since it's already in main. Clean 2-commit history now.

Add Mistral as a first-class provider alongside the 17 existing ones.
Mistral uses the OpenAI-compatible API at https://api.mistral.ai/v1
with provider-specific model prefix stripping (mistral/model → model).

Changes:
- Add Mistral to ProvidersConfig, IsEmpty(), HasProvidersConfig()
- Add mistral entry in default model_list (defaults.go)
- Add mistral protocol in factory_provider.go and getDefaultAPIBase()
- Add mistral prefix stripping in openai_compat normalizeModel()
- Add mistral case in legacy factory.go resolveProviderSelection()
- Add mistral migration entry in ConvertProvidersToModelList()
- Add mistral to supported providers in migrate/config.go
- Add mistral section in config.example.json
- Update AllProviders test (17 → 18 providers)

Tested end-to-end with mistral-small-latest model.
Mistral's API strictly validates tool_calls in assistant messages and
rejects non-standard fields. The ToolCall struct had Name and Arguments
as top-level JSON fields, duplicating data already in Function.Name
and Function.Arguments. OpenAI silently ignored these extras but
Mistral returns 422.

Change json tags to "-" so these internal fields are no longer
serialized to API payloads while remaining available in Go code.
@nikolasdehor
Copy link

This addresses the feature request in #246 (Add Mistral AI provider support). Linking for tracking.

waplaysoft pushed a commit to waplaysoft/picoclaw that referenced this pull request Feb 23, 2026
sipeed#541)

This is a cleaner implementation than the previous attempt, adding Mistral as a first-class provider.

Changes from PR sipeed#541:
- Add Mistral to ProvidersConfig, IsEmpty(), HasProvidersConfig()
- Add mistral entry in default model_list (defaults.go)
- Add mistral protocol in factory_provider.go and getDefaultAPIBase()
- Add mistral prefix stripping in openai_compat normalizeModel()
- Add mistral case in legacy factory.go resolveProviderSelection()
- Add mistral migration entry in ConvertProvidersToModelList()
- Add mistral to supported providers in migrate/config.go
- Add mistral section in config.example.json
- Fix: remove extra fields from ToolCall JSON serialization (Mistral strict validation)

Previously attempted changes (b42039f, 3555fbc) reverted in favor of this implementation.
waplaysoft pushed a commit to waplaysoft/picoclaw that referenced this pull request Feb 23, 2026
…stream PR sipeed#198)

Implements file and image support for chat channels, following
pattern from nanobot implementation. Images are downloaded to local
temp files, base64-encoded, and sent as multipart content to LLMs
with vision capabilities.

Changes:
- Modified Message.Content from string to interface{} to support multipart content
- Added buildUserContent() method for base64 encoding images
- Updated all providers to handle multipart content
- Added IsImageFile() and improved GetMimeType() utilities
- Implemented image download in Discord, LINE, and Slack channels
- Fixed critical bug where media paths weren't reaching LLM
- Consolidated duplicate contentToString* functions
- Proper file cleanup after base64 encoding

Conflicts resolved to preserve:
- Mistral provider support (PR sipeed#541)
- ContextWindow/MaxTokens separation

Note: PR sipeed#198 was made on an older upstream commit and required manual conflict resolution.
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.

7 participants