Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ env:
CACHE_SALT: v2-vmlx-5b84387
# Pin Xcode so cache keys are stable across runner image bumps. When you
# need to upgrade, change here AND in setup-xcode below.
XCODE_VERSION: "26.3"
XCODE_VERSION: "26.4.1"

jobs:
test-core:
Expand Down Expand Up @@ -333,7 +333,7 @@ jobs:
uses: actions/checkout@v5

# OsaurusCLI's Package.swift requires `swift-tools-version: 6.2`, which
# ships with Xcode 26.3. The runner's default `swift` may be older
# ships with the pinned Xcode. The runner's default `swift` may be older
# (currently 6.1), so point xcrun at the pinned Xcode for these steps.
# GitHub Actions doesn't let job-level `env:` reference workflow-level
# `env:`, so keep XCODE_VERSION in sync here if it ever changes.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// osaurus
//
// Model for MCPB (MCP Bundle) manifest.json files.
// Supports both standard MCPB format and Claude Desktop integration format.
// Supports both standard MCPB format and desktop-client integration format.
//

import Foundation
Expand All @@ -12,7 +12,7 @@ struct MCPBundleManifest: Codable {
// Standard MCPB format
let mcpVersion: String?

// Claude Desktop format
// Desktop-client integration format
let manifestVersion: String?

let name: String
Expand All @@ -21,7 +21,7 @@ struct MCPBundleManifest: Codable {
let description: String?
let entry: EntryPoint?

// Claude Desktop format
// Desktop-client integration format
let server: ServerConfig?
let icon: String?

Expand Down Expand Up @@ -81,7 +81,7 @@ struct MCPBundleManifest: Codable {
return (entry.command, entry.args, entry.env)
}

// Try Claude Desktop format
// Try desktop-client integration format
if let server = server, let config = server.mcpConfig {
return (config.command, config.args, config.env)
}
Expand Down
2 changes: 1 addition & 1 deletion Packages/OsaurusCore/Models/API/AnthropicAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// AnthropicAPI.swift
// osaurus
//
// Anthropic Messages API compatible request/response models for Claude Code compatibility.
// Anthropic Messages API compatible request/response models for SDK-compatible clients.
//

import Foundation
Expand Down
2 changes: 1 addition & 1 deletion Packages/OsaurusCore/Models/Chat/ChatConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ public struct ChatConfiguration: Codable, Equatable, Sendable {
// MARK: - Tool Settings
/// When true, no tools or preflight context are passed to the model. The raw message is sent
/// directly, keeping the prompt stable across turns for maximum KV-cache reuse. Recommended
/// when osaurus is acting as a plain LLM backend for an external agent (e.g. Claude via API).
/// when osaurus is acting as a plain LLM backend for an external agent.
public var disableTools: Bool

/// Default tool selection mode for the built-in Default agent (nil => .auto).
Expand Down
9 changes: 5 additions & 4 deletions Packages/OsaurusCore/Models/Configuration/ModelOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -161,14 +161,15 @@ struct QwenThinkingProfile: ModelProfile {

// MARK: - Auto Thinking Profile (chat-template driven)

/// Fallback profile that activates for any locally-installed model whose
/// chat template reads an `enable_thinking` kwarg. Registered last so that
/// explicit family profiles (Qwen, Venice, etc.) still win when they match.
/// Fallback profile that activates for locally-installed models whose chat
/// template exposes an `enable_thinking` kwarg and uses thinking markers the
/// runtime can process. Registered last so that explicit family profiles
/// (Qwen, Venice, etc.) still win when they match.
struct AutoThinkingProfile: ModelProfile {
static let displayName = "Thinking"

static func matches(modelId: String) -> Bool {
LocalReasoningCapability.capability(forModelId: modelId).hasEnableThinkingKwarg
LocalReasoningCapability.capability(forModelId: modelId).isToggleableThinking
}

static let options: [ModelOptionDefinition] = [
Expand Down
31 changes: 16 additions & 15 deletions Packages/OsaurusCore/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,24 +74,31 @@ let package = Package(
// Swift's Clang importer sees two different definitions
// of those typedefs and rejects the build with
// 'Fts5ExtensionApi' has different definitions in different modules
// The fix is two-part:
// The fix is three-part:
// 1. `include/sqlite3.h` wraps the `_FTS5_H` block in
// `#ifndef OSAURUS_OMIT_FTS5_HEADERS` (search for
// OSAURUS LOCAL MODIFICATION inside that file).
// 2. The `cSettings` `.define("OSAURUS_OMIT_FTS5_HEADERS")`
// below activates the wrap.
// 2. `include/OsaurusSQLCipher.h` defines
// `OSAURUS_OMIT_FTS5_HEADERS` before including
// sqlite3.h so Swift's Clang module import sees the
// hidden extension API.
// 3. The `cSettings` `.define("OSAURUS_OMIT_FTS5_HEADERS")`
// below keeps the C compilation path aligned.
// `sqlite3.c` itself inlines its own copy of the header
// text, so FTS5's SQL-level functionality keeps working;
// we only hide the C-extension API, which Osaurus
// doesn't use.
// `Tests/Storage/SQLCipherVendorGuardTests.swift` asserts
// both the header guard and the cSettings flag are in
// place — CI fails if a SQLCipher bump strips the guard.
// the header guard, umbrella define, and cSettings flag
// are in place — CI fails if a SQLCipher bump strips them.
//
// ⚠️ `sqlite3ext.h` struct collision. A similar issue occurs
// with `struct sqlite3_api_routines` in `sqlite3ext.h`.
// We wrap its contents in `#ifndef OSAURUS_OMIT_SQLITE3EXT_HEADERS`
// and define it below.
// ⚠️ sqlite3ext.h collision. Newer macOS SDKs append fields
// to `sqlite3_api_routines` before our pinned SQLCipher
// adopts that SQLite version. Osaurus does not compile
// SQLite loadable extensions, so the umbrella header hides
// sqlite3ext.h's loadable-extension API from the Swift
// Clang importer while still including the header to keep
// module import warnings quiet.
.target(
name: "OsaurusSQLCipher",
path: "SQLCipher",
Expand Down Expand Up @@ -128,12 +135,6 @@ let package = Package(
// copy of sqlite3.h text is unaffected, so the C
// compilation of FTS5 keeps working.
.define("OSAURUS_OMIT_FTS5_HEADERS"),
// Hide the `sqlite3_api_routines` struct from
// `include/sqlite3ext.h` so the Swift Clang importer
// doesn't conflict with the system SQLite3 module.
// `sqlite3.c`'s inlined copy of sqlite3ext.h text is
// unaffected.
.define("OSAURUS_OMIT_SQLITE3EXT_HEADERS"),
// The SQLite amalgamation calls a few self-references
// before their forward declarations show up; modern
// Apple clang upgrades this from a warning to an
Expand Down
49 changes: 28 additions & 21 deletions Packages/OsaurusCore/SQLCipher/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,33 +77,40 @@ After copying a fresh `sqlite3.h` over the top, re-apply by:
#endif /* OSAURUS_OMIT_FTS5_HEADERS — END OSAURUS LOCAL MODIFICATION */
```

Similarly, `include/sqlite3ext.h` has an OSAURUS LOCAL MODIFICATION that
wraps the entire file in `#ifndef OSAURUS_OMIT_SQLITE3EXT_HEADERS` to
prevent `struct sqlite3_api_routines` from colliding with Apple's system
`SQLite3` module.
3. Confirm `OsaurusSQLCipher.h` still defines
`OSAURUS_OMIT_FTS5_HEADERS` before it includes `sqlite3.h`, and
`Package.swift` still defines the same flag in the
`OsaurusSQLCipher` target `cSettings`.

After copying a fresh `sqlite3ext.h` over the top, re-apply by:

1. Find the `#ifndef SQLITE3EXT_H` line. Insert this block immediately
after `#define SQLITE3EXT_H`:
The amalgamation `sqlite3.c` itself is **not** modified — it inlines
its own copy of sqlite3.h text, so the C compilation of FTS5 keeps
working.

```c
#ifndef OSAURUS_OMIT_SQLITE3EXT_HEADERS
```
### Re-applying the sqlite3ext import guard

2. Find the matching `#endif /* SQLITE3EXT_H */` line at the end. Insert
immediately before it:
`include/sqlite3ext.h` has an OSAURUS LOCAL MODIFICATION that hides the
loadable-extension API behind `OSAURUS_OMIT_SQLITE_EXTENSION_API`.
`include/OsaurusSQLCipher.h` defines that macro before including
`sqlite3ext.h`.

```c
#endif /* OSAURUS_OMIT_SQLITE3EXT_HEADERS — END OSAURUS LOCAL MODIFICATION */
```
This exists because Osaurus imports SQLCipher from Swift for the core
SQLite API, not for compiling SQLite loadable extensions. Apple's
system `SQLite3` module is also imported by dependencies in the same
Swift build. When newer macOS SDKs append fields to
`sqlite3_api_routines` before this pinned SQLCipher version adopts the
same upstream SQLite release, Swift's Clang importer rejects the build
with a cross-module struct-definition mismatch.

3. Run `swift build`. If anything red, see the explainer in
`sqlcipher_amalgamation.c`.
After copying a fresh `sqlite3ext.h` over the top, re-apply by:

The amalgamation `sqlite3.c` itself is **not** modified — it inlines
its own copy of sqlite3.h text, so the C compilation of FTS5 keeps
working.
1. Find `#include "sqlite3.h"` near the top of the file. Insert the
`#ifndef OSAURUS_OMIT_SQLITE_EXTENSION_API` block immediately after
it.
2. Insert the matching close immediately before the final
`#endif /* SQLITE3EXT_H */`.
3. Confirm `OsaurusSQLCipher.h` still defines
`OSAURUS_OMIT_SQLITE_EXTENSION_API` immediately before it includes
`sqlite3ext.h`.

## Why CommonCrypto?

Expand Down
20 changes: 12 additions & 8 deletions Packages/OsaurusCore/SQLCipher/include/OsaurusSQLCipher.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,22 +30,26 @@
#define SQLITE_HAS_CODEC 1
#endif

#ifndef OSAURUS_OMIT_FTS5_HEADERS
#define OSAURUS_OMIT_FTS5_HEADERS 1
#endif

#include "sqlite3.h"
/* `sqlite3ext.h` lives in the same `include/` dir alongside us, so
* Clang's umbrella-header consistency check requires it to either
* be included from this umbrella or excluded via a module map.
* Pulling it in here is harmless (it's just the loadable-extension
* SQLite API) and silences the
* We include it with the loadable-extension API hidden because
* Osaurus does not compile SQLite loadable extensions, and newer
* macOS SDKs may expose extension fields that SQLCipher 4.6.1 does
* not. This silences the
*
* warning: umbrella header for module 'OsaurusSQLCipher' does not
* include header 'sqlite3ext.h'
*
* that would otherwise fire on every Swift import of this module.
* We define `OSAURUS_OMIT_SQLITE3EXT_HEADERS` to hide the
* `sqlite3_api_routines` struct from the Swift Clang importer
* so it doesn't conflict with Apple's system `SQLite3` module. */
#ifndef OSAURUS_OMIT_SQLITE3EXT_HEADERS
#define OSAURUS_OMIT_SQLITE3EXT_HEADERS 1
* without reintroducing Swift Clang-importer type collisions against
* Apple's system SQLite3 module. */
#ifndef OSAURUS_OMIT_SQLITE_EXTENSION_API
#define OSAURUS_OMIT_SQLITE_EXTENSION_API 1
#endif
#include "sqlite3ext.h"

Expand Down
27 changes: 16 additions & 11 deletions Packages/OsaurusCore/SQLCipher/include/sqlite3ext.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,27 @@
*/
#ifndef SQLITE3EXT_H
#define SQLITE3EXT_H
#include "sqlite3.h"

/*
* BEGIN OSAURUS LOCAL MODIFICATION (do not remove on SQLCipher
* version bumps; re-apply after copying a new sqlite3ext.h).
*
* The `sqlite3_api_routines` struct collides with the same
* struct from Apple's system `SQLite3` module when both modules
* end up imported in the same Swift compilation unit. We hide
* the entire contents from the Clang importer here (activated
* by `OSAURUS_OMIT_SQLITE3EXT_HEADERS` in `Package.swift`).
* `sqlite3.c` includes its own copy of this text so the C
* compilation is unaffected.
* Osaurus imports SQLCipher from Swift for the core SQLite API, not
* for compiling SQLite loadable extensions. Apple's system SQLite3
* module is also imported by dependencies in the same Swift build,
* and modern SDKs may append fields to `sqlite3_api_routines` before
* SQLCipher has adopted the same upstream SQLite version. Swift's
* Clang importer treats those cross-module struct differences as a
* build error.
*
* The umbrella header defines OSAURUS_OMIT_SQLITE_EXTENSION_API
* before including this file so the module stays warning-free without
* exporting the loadable-extension API that Osaurus does not use.
* The SQLCipher amalgamation is unaffected because sqlite3.c inlines
* its own sqlite3ext.h text.
*/
#ifndef OSAURUS_OMIT_SQLITE3EXT_HEADERS

#include "sqlite3.h"
#ifndef OSAURUS_OMIT_SQLITE_EXTENSION_API

/*
** The following structure holds pointers to all of the SQLite API
Expand Down Expand Up @@ -731,6 +736,6 @@ typedef int (*sqlite3_loadext_entry)(
# define SQLITE_EXTENSION_INIT3 /*no-op*/
#endif

#endif /* OSAURUS_OMIT_SQLITE3EXT_HEADERS — END OSAURUS LOCAL MODIFICATION */
#endif /* OSAURUS_OMIT_SQLITE_EXTENSION_API - END OSAURUS LOCAL MODIFICATION */

#endif /* SQLITE3EXT_H */
5 changes: 4 additions & 1 deletion Packages/OsaurusCore/Services/LocalReasoningCapability.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,15 @@ enum LocalReasoningCapability {
struct Capability: Sendable {
/// Template references `<think>` or `</think>` tags.
let supportsThinking: Bool
/// Template reads an `enable_thinking` kwarg — i.e. thinking is toggleable.
/// Template reads an `enable_thinking` kwarg.
let hasEnableThinkingKwarg: Bool
/// Template itself injects a literal `<think>` opener into the assistant prompt
/// tail, which means the model's generated stream will only contain the closing
/// `</think>` and needs a middleware prepend for the UI tag parser to work.
let templateInjectsThinkTag: Bool
/// True when the template both exposes a toggle kwarg and uses
/// reasoning markers the runtime recognizes.
var isToggleableThinking: Bool { supportsThinking && hasEnableThinkingKwarg }

static let none = Capability(
supportsThinking: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,9 @@ struct ModelProfileRegistryTests {

@Test("Non-reasoning Gemma variants do not get a thinking toggle")
func gemma_noThinkingToggle() {
let profile = ModelProfileRegistry.profile(for: "gemma-4-e2b-it-4bit")
// Gemma can match an image-options profile but should never expose
// the thinking toggle because it doesn't honor `enable_thinking`.
let profile = ModelProfileRegistry.profile(for: "gemma-2-non-reasoning-\(UUID().uuidString)")
// Use a guaranteed-missing suffix so this stays independent of the
// developer's locally installed model directory.
#expect(profile?.thinkingOption == nil)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ struct LocalReasoningCapabilityTests {
#expect(cap.supportsThinking)
#expect(cap.hasEnableThinkingKwarg)
#expect(cap.templateInjectsThinkTag)
#expect(cap.isToggleableThinking)
}

@Test("Qwen3-style: supports thinking but no template-side injection")
Expand All @@ -35,6 +36,7 @@ struct LocalReasoningCapabilityTests {
#expect(cap.supportsThinking)
#expect(cap.hasEnableThinkingKwarg)
#expect(!cap.templateInjectsThinkTag)
#expect(cap.isToggleableThinking)
}

@Test("Non-reasoning template: all signals false")
Expand All @@ -46,6 +48,21 @@ struct LocalReasoningCapabilityTests {
#expect(!cap.supportsThinking)
#expect(!cap.hasEnableThinkingKwarg)
#expect(!cap.templateInjectsThinkTag)
#expect(!cap.isToggleableThinking)
}

@Test("enable_thinking alone is not enough for a UI toggle")
func enableThinkingWithoutRecognizedThinkingMarkers() {
let template = """
{%- if enable_thinking is defined and enable_thinking -%}
{{- '<|reason|>' -}}
{%- endif -%}
"""
let cap = LocalReasoningCapability.analyze(template: template)
#expect(!cap.supportsThinking)
#expect(cap.hasEnableThinkingKwarg)
#expect(!cap.templateInjectsThinkTag)
#expect(!cap.isToggleableThinking)
}

@Test("GLM-flash style: emits </think> without injection (middleware-needed)")
Expand All @@ -60,6 +77,7 @@ struct LocalReasoningCapabilityTests {
#expect(cap.supportsThinking)
#expect(!cap.hasEnableThinkingKwarg)
#expect(!cap.templateInjectsThinkTag)
#expect(!cap.isToggleableThinking)
}

/// Gemma-4's chat_template.jinja opens thinking with the pipe-wrapped
Expand All @@ -83,6 +101,7 @@ struct LocalReasoningCapabilityTests {
#expect(cap.supportsThinking)
#expect(cap.hasEnableThinkingKwarg)
#expect(cap.templateInjectsThinkTag)
#expect(cap.isToggleableThinking)
}

// MARK: - jang_config.json chat.reasoning fallback (DSV4-class bundles)
Expand Down Expand Up @@ -123,6 +142,7 @@ struct LocalReasoningCapabilityTests {
// Python encoder takes `thinking_mode` as a positional argument
// instead, so the kwarg flag stays false.
#expect(cap?.hasEnableThinkingKwarg == false)
#expect(cap?.isToggleableThinking == false)
// DSV4's template is outside the bundle (Python module) — vmlx
// injects the thinking tag itself when the caller picks thinking
// mode. From osaurus's perspective there is no on-disk Jinja to
Expand Down
Loading
Loading