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
15 changes: 13 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,12 @@ jobs:
python-version: ${{ matrix.python-version }}

- name: Install (hermetic deps only)
# Install httpx + pytest and the package itself without pulling the heavy
# Install test dependencies without pulling the heavy
# llama-cpp-python build. Runtime tests inject a fake llama_cpp module,
# so CI needs neither the native extension nor a model download.
run: |
python -m pip install --upgrade pip
python -m pip install httpx pytest
python -m pip install httpx pytest Pillow
python -m pip install -e . --no-deps

- name: Run hermetic tests
Expand All @@ -96,7 +96,13 @@ jobs:
tests/test_cli_auth.py \
tests/test_remote_inference.py \
tests/test_desktop_sdk.py \
tests/test_inputs.py \
tests/test_image_content.py \
tests/test_runtime_registry_sdk.py \
tests/test_vision_metadata.py \
tests/test_vision_assets.py \
tests/test_vision_runtime.py \
tests/test_vision_result.py \
tests/test_sdk.py::TestInstallAndImport

local-files-windows:
Expand Down Expand Up @@ -175,3 +181,8 @@ jobs:
run("-I", "-c", "import programasweights, llama_cpp; "
"print(programasweights.__version__, llama_cpp.__version__); "
"print(llama_cpp.llama_print_system_info().decode())")
run("-m", "pip", "install", str(wheel) + "[vision]", "--extra-index-url",
"https://pypi.programasweights.com/simple/", "--only-binary=llama-cpp-python")
run("-m", "pip", "check")
run("-I", "-c", "import PIL, llama_cpp.mtmd_cpp; "
"from llama_cpp.llama_chat_format import MTMDChatHandler")
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## 0.4.12 (2026-10-07)

- Add local Qwen3.5-0.8B functions with ordered text and `paw.Image` inputs,
including compatible image `.paw` bundles.
- Add image dependencies through `programasweights[vision]`.
- Add `return_info=True` for immutable results with finish reason, token counts,
and elapsed call time. Results can be returned from multiprocessing workers.

## 0.4.11 (2026-10-04)

- Support llama-cpp-python 0.3.36.
Expand Down
29 changes: 29 additions & 0 deletions PYPI_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,35 @@ Advanced adapter-free inference is available with
`paw.function(None, interpreter="gpt2")`; see the Python API reference for
its intentionally strict semantics.

## Local image functions

Install the optional image dependencies:

```bash
pip install "programasweights[vision]" --extra-index-url https://pypi.programasweights.com/simple/
```

Prompt Qwen3.5-0.8B without an adapter:

```python
import programasweights as paw

with paw.function(None, interpreter="Qwen/Qwen3.5-0.8B") as describe:
print(describe("Describe this image.", paw.Image("photo.png"), max_tokens=128))
```

Or load a compatible image `.paw` bundle with `paw.function("./locator.paw")`.
Image functions accept ordered positional strings and `paw.Image` objects;
generation options are keyword-only. `paw.Image` snapshots a local path,
encoded bytes, or a Pillow image. The SDK keeps native image dimensions and
composites transparency over white; the model backend constructs image patches.

The first load downloads the exact model and projector. Use `offline=True`
after caching both files. Matching model/projector hashes and runtime settings
share one local runtime; calls reset model state and select the correct LoRA.
Use context managers or `close()` to release it. Image inference is local only;
this SDK change does not add a hosted image compiler or remote image inference.

## Browser SDK

Programs compiled with GPT-2 also run in the browser via WebAssembly. The initial model and program assets download automatically; inference then runs client-side.
Expand Down
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,35 @@ Advanced adapter-free inference is available with
[Python API reference](docs/api-reference/python-sdk.md#advanced-adapter-free-base-interpreter)
for its intentionally strict semantics.

## Local image functions

Install the optional image dependencies:

```bash
pip install "programasweights[vision]" --extra-index-url https://pypi.programasweights.com/simple/
```

Prompt Qwen3.5-0.8B without an adapter:

```python
import programasweights as paw

with paw.function(None, interpreter="Qwen/Qwen3.5-0.8B") as describe:
print(describe("Describe this image.", paw.Image("photo.png"), max_tokens=128))
```

Or load a compatible image `.paw` bundle with `paw.function("./locator.paw")`.
Image functions accept ordered positional strings and `paw.Image` objects;
generation options are keyword-only. `paw.Image` snapshots a local path,
encoded bytes, or a Pillow image. The SDK keeps native image dimensions and
composites transparency over white; the model backend constructs image patches.

The first load downloads the exact model and projector. Use `offline=True`
after caching both files. Matching model/projector hashes and runtime settings
share one local runtime; calls reset model state and select the correct LoRA.
Use context managers or `close()` to release it. Image inference is local only;
this SDK change does not add a hosted image compiler or remote image inference.

## Browser SDK

Programs compiled with GPT-2 also run in the browser via WebAssembly. The initial model and program assets download automatically; inference then runs client-side.
Expand Down
104 changes: 97 additions & 7 deletions docs/api-reference/python-sdk.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ bundle directly. Required runtime metadata and base models are cached for reuse.
| `verbose` | Enable verbose logging (default `False`). |
| `offline` | Use only local files/cache and make zero network calls; fail if required validated assets are missing. `PAW_OFFLINE=1` has the same effect. |
| `remote` | Run hosted inference without downloading model assets (default `False`). Accepts a `Program` object, ID, or slug. Cannot be combined with offline mode, local file paths, `interpreter`, or non-default local runtime options. |
| `interpreter` | Advanced adapter-free mode only. Must be passed by keyword and only when `program_id` is explicitly `None`. Supported values are `Qwen/Qwen3-0.6B` and `gpt2`. |
| `interpreter` | Advanced adapter-free mode only. Must be passed by keyword and only when `program_id` is explicitly `None`. Supported values are `Qwen/Qwen3-0.6B`, `gpt2`, and `Qwen/Qwen3.5-0.8B`. |

For local inference, the returned callable accepts:
For local text-only programs, the returned callable accepts:

```python
output: str = fn(input_text, max_tokens=None, temperature=0.0, logits_processor=None)
Expand All @@ -63,7 +63,9 @@ This advanced hook is not built-in regex or JSON-schema validation. Each process

Compiled mode is strict: the adapter, prompt template, matching metadata,
runtime manifest, and runtime-compatible base-model file must all validate. Version 0.4.5
accepts runtime manifest version 1 with `adapter_format="gguf_lora"`.
introduced runtime manifest version 1 with `adapter_format="gguf_lora"`.
Image bundles use version 2 and additionally pin the projector, ordered-content
prompt contract, and preprocessing settings.
Built-in models are checked against pinned size/SHA-256 metadata and GGUF
magic. Historical manifests for those known runtime IDs are normalized to the
same canonical integrity metadata, so missing server-side checksum fields
Expand Down Expand Up @@ -144,10 +146,10 @@ This mode is intentionally explicit:
- `program_id=""` raises `ValueError` and explains that base mode requires explicit `None`.
- A non-empty program reference together with `interpreter` raises `ValueError`.
- No PAW API, slug lookup, program download, adapter load, or disk prefix cache is used.
- Online mode may download only the selected base GGUF from its built-in runtime manifest. Offline mode never downloads.
- Every invocation resets model state, renders the complete prompt, and tokenizes that complete rendered prompt in one call.
- Online mode may download the selected base GGUF and, for image interpreters, its projector from the built-in runtime manifest. Offline mode never downloads.
- Every invocation resets model state. Text interpreters render and tokenize the complete text prompt; the image interpreter processes ordered text/image content.

The built-in prompt contract is versioned with each runtime manifest and must contain exactly one `{INPUT_PLACEHOLDER}`:
The built-in text prompt contracts are versioned with each runtime manifest and contain exactly one `{INPUT_PLACEHOLDER}`:

```text
# Qwen/Qwen3-0.6B
Expand All @@ -168,6 +170,94 @@ The Qwen bytes are the exact raw-user rendering of
Zero-token prompts and prompts that consume the full context window raise
`ValueError`.

### Local text/image calls

Install `programasweights[vision]` to use Qwen3.5 image inference. The public
call accepts ordered strings and `paw.Image` objects:

```python
with paw.function(None, interpreter="Qwen/Qwen3.5-0.8B") as compare:
answer = compare(
"Before:", paw.Image("before.png"),
"After:", paw.Image("after.png"),
"What changed?",
max_tokens=128,
)
```

The adapter-free function sends one user message containing these parts, with
no injected system message. The pinned model chat template adds the assistant
prefix with thinking disabled. It also accepts text-only calls. Each call is
independent; no conversation, image, or recurrent-state cache carries across calls.

A compatible image bundle loads through `paw.function("./locator.paw")` and
uses the same call interface. Its `prompt_template.txt` provides literal system
instructions; `{INPUT_PLACEHOLDER}` has no substitution meaning in that file.
Bundle loading still requires a valid adapter and matching runtime metadata.

```python
output: str = fn(
*parts,
max_tokens=None,
temperature=0.0,
logits_processor=None,
response_format=None,
return_info=False,
)
```

Generation options are keyword-only for image functions. `response_format`
passes a llama.cpp-compatible JSON response-format specification to the backend;
always validate returned data, especially when `max_tokens` truncates the output.
`max_tokens=0` returns an empty string without generation. Empty input lists and
parts other than strings or `paw.Image` are rejected; unpack an existing list
with `fn(*parts)`.

Pass `return_info=True` to receive a `paw.FunctionResult` instead of a string:

```python
result = fn("Find the target:", paw.Image("scene.png"), return_info=True)
print(result.text)
print(result.finish_reason) # Backend reason, e.g. "stop" or "length"; otherwise None.
print(result.usage) # Backend token counts, or None when unavailable.
print(result.elapsed_seconds) # Full call duration through native cleanup.
```

The result is immutable and belongs to that call. `usage`, when present, is a
copied, read-only mapping; use `dict(result.usage)` when logging it as JSON.
Counts have the backend's semantics; they are not an estimate of image patches
or billable tokens. `elapsed_seconds` uses a monotonic clock and includes input
preparation, waiting for function/runtime locks, adapter selection, inference,
and native cleanup. It excludes loading the function and constructing input
`paw.Image` objects before the call.

`return_info` is a keyword-only boolean and defaults to `False`, preserving
string returns. With `max_tokens=0`, the opt-in result has empty text and `None`
for both backend fields, because no generation ran. Truncated text is still
returned with the backend's `"length"` reason; errors still raise normally.
This option applies to the local Qwen3.5 image runtime, including text-only calls
to that interpreter and compiled image programs. Existing text runtimes and
remote inference do not accept this option.

`paw.Image(source)` accepts a regular local path, encoded image bytes, or a
Pillow image and snapshots the supplied content. It does not fetch URLs. Images
are decoded locally and converted to RGB PNG without SDK resizing, EXIF rotation,
or ICC conversion. Transparency is composited over white. The backend uses the
manifest's image-token limits to construct patches; high-bit-depth and floating
point inputs require explicit conversion by the caller.

Live functions share a model/projector only when the exact asset hashes,
preprocessing, context size, GPU settings, and verbosity match. Shared calls
serialize and switch adapters; closing one function leaves other users of that
runtime usable. Close the last function to release native resources. Native
runtimes must not be inherited through `fork`; use a spawned process instead.

The built-in image interpreter pins a Q8_0 model and BF16 projector by repository
revision, size, and SHA-256. Existing bundles retain their own exact asset
identities; naming the same upstream model does not make different GGUF files
interchangeable. Offline mode requires both verified files. Remote image
inference and hosted image compilation are not introduced by this interface.

## Preparing programs for offline use

```python
Expand All @@ -178,7 +268,7 @@ ready = paw.is_offline_ready("da03/my-classifier") # local check; no network
cached = paw.list_cached_programs()
```

`prepare_program` resolves and downloads the program, runtime manifest, and shared base model without retaining a loaded `PawFunction`. Pass `offline=True` to require an already complete local cache and prohibit network access.
`prepare_program` resolves and downloads the program, runtime manifest, and shared base model (plus the projector for image programs) without retaining a loaded function. Pass `offline=True` to require an already complete local cache and prohibit network access.

Desktop applications can receive structured progress without parsing stderr:

Expand Down
46 changes: 37 additions & 9 deletions programasweights/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@
from importlib.metadata import version as _meta_version
__version__ = _meta_version("programasweights")
except Exception:
__version__ = "0.4.11"
__version__ = "0.4.12"

from ._inputs import Image
from ._result import FunctionResult
from ._output import ProgressCallback, ProgressEvent, report_progress
from .cache import CachedProgram
from .client import (
Expand Down Expand Up @@ -332,7 +334,11 @@ def prepare_program(
},
)

if offline:
if cache.declares_vision(meta):
from ._vision_assets import get_vision_asset_paths

get_vision_asset_paths(runtime_manifest, offline=offline, progress=progress)
elif offline:
base_model_path = cache.get_cached_base_model_path(runtime_manifest)
if base_model_path is None:
raise RuntimeError(
Expand Down Expand Up @@ -445,11 +451,15 @@ def function(
program IDs, slugs, pinned versions, and Program objects. Cannot
be combined with offline mode or non-default local runtime options.
interpreter: Advanced adapter-free mode. This is only valid when
``program_id`` is explicitly ``None``. Initially supported values
are ``"Qwen/Qwen3-0.6B"`` and ``"gpt2"``.
``program_id`` is explicitly ``None``. Supported values are
``"Qwen/Qwen3-0.6B"``, ``"gpt2"``, and ``"Qwen/Qwen3.5-0.8B"``.
Qwen3.5 accepts ordered text/image parts as one user message,
without a system prompt or an adapter.

Returns:
A callable that takes an input string and returns an output string.
A callable returning an output string. Text programs accept one input
string. Image programs accept ordered positional strings and paw.Image
objects, with generation options passed by keyword.

Example:
>>> fn = paw.function("email-triage")
Expand All @@ -463,6 +473,9 @@ def function(
>>> fn = paw.function("./classifier.paw") # local bundle

>>> base = paw.function(None, interpreter="gpt2")

>>> vision = paw.function(None, interpreter="Qwen/Qwen3.5-0.8B")
>>> vision("Describe this image.", paw.Image("photo.png"))
"""
import os
from . import cache
Expand Down Expand Up @@ -492,10 +505,16 @@ def function(
raise ValueError(
"program_id=None requires an explicit interpreter."
)
cache.get_base_runtime_manifest(interpreter)
from .runtime_llamacpp import PawFunction
runtime_manifest = cache.get_base_runtime_manifest(interpreter)
if cache.declares_vision({"runtime": runtime_manifest}):
from ._runtime_vision import VisionFunction

return PawFunction.from_base(
function_type = VisionFunction
else:
from .runtime_llamacpp import PawFunction

function_type = PawFunction
return function_type.from_base(
interpreter,
n_ctx=n_ctx,
n_gpu_layers=n_gpu_layers,
Expand Down Expand Up @@ -549,7 +568,15 @@ def function(
)
program_dir = cache.get_program_dir(resolved_id)

return PawFunction(
import json

function_type = PawFunction
meta = json.loads((program_dir / "meta.json").read_text(encoding="utf-8"))
if cache.declares_vision(meta):
from ._runtime_vision import VisionFunction

function_type = VisionFunction
return function_type(
program_dir,
n_ctx=n_ctx,
n_gpu_layers=n_gpu_layers,
Expand Down Expand Up @@ -704,6 +731,7 @@ def list_compilers() -> list[dict]:
"CompileJob",
"CompilePrecheck",
"CompileStatus",
"FunctionResult",
"Program",
"ProgressCallback",
"ProgressEvent",
Expand Down
Loading
Loading