feat: tool calling and in-JVM embeddings (0.3.0) - #106
Merged
Conversation
Adds two capabilities the runtime lacked, plus the release metadata for 0.3.0. Tool calling. ToolSpec and ToolCall describe declarations and invocations while keeping argument text opaque, so models-api stays dependency-free: both target frameworks want raw JSON strings, and locating an argument's extent needs only brace matching. ToolSyntax records how each family expresses calls, taken from its published chat template, and drives rendering and recovery from one descriptor. llama.cpp reached the same conclusion from the other direction, collapsing roughly twenty hand-written family parsers into a small taxonomy. Only verified families are populated. Gemma 4 and MiniCPM5 have real formats but encode arguments as tagged pairs that carry no type information, so producing JSON from them needs the declared schemas; they report supportsTools() but not canParseToolCalls(), and adapters gate on the latter. Tool results are attacker-influenced in a way ordinary chat text is not, so delimiters render as CONTROL segments and results as TEXT. Because the tokenizer recognises registered tokens only inside CONTROL, a result spelling out control markers cannot forge a turn. Verified against the real Qwen3 vocabulary, where <tool_call> is one token (151657) as CONTROL and four as TEXT: the secure path and the training-faithful path are the same path. Embeddings. An embedding is the generative pass with its head removed, so Head replaces the boolean that conflated "project to vocabulary" with "is this activation needed at all". Prefill legitimately skips most of the final layer when the activation is discarded; embedding needs precisely that activation. Both pruning shortcuts had to be excluded, including one that skips the last layer's FFN, which a second boolean would have hidden. EmbeddingBackend moves to models-api because it is a contract. Leaving it in models-embedding would have forced every backend implementing it to depend on vectors-db, inverting the models -> vectors tier direction. Sampler replaces a full-vocabulary sort with a bounded-heap top-k selection, 19.251 ms to 0.848 ms per sampled token at a 151,936-token vocabulary. Ties still prefer the lower token id, so seeded output is unchanged; a 500-trial oracle test pins equivalence against the previous implementation. Adds integration coverage against real Qwen3-Embedding-0.6B weights: related text lands closer than unrelated with a >0.25 margin, and both framework adapters produce byte-identical vectors from one backend. BREAKING CHANGE: EmbeddingBackend moved from com.integrallis.models.embedding to com.integrallis.models.api. No published artifact implemented it.
…ibility JsonRawSchema postdates LangChain4j 1.0.0, which this module still compiles against, so naming it directly broke the oldest supported version in the compatibility matrix. Detect it structurally and read its payload reflectively; the test builds one the same way and skips where the type is absent. Verified against the exact CI invocations for LangChain4j 1.0.0, 1.13.1 and 1.17.2, and Spring AI 1.1.4, 1.1.8 and 2.0.0.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds two capabilities the runtime lacked, and prepares the 0.3.0 release.
Tool calling
ToolSpecandToolCalldescribe declarations and invocations while keeping argument text opaque — both target frameworks want raw JSON strings, and locating an argument's extent needs only brace matching, somodels-apistays dependency-free.ToolSyntaxrecords how each family expresses calls, read from its published chat template, and drives both rendering and recovery from one descriptor. llama.cpp reached the same conclusion from the other direction, collapsing ~20 hand-written family parsers into a small inferred taxonomy.Only verified families are populated. Gemma 4 and MiniCPM5 have real formats, but encode arguments as tagged pairs carrying no type information —
<param name="n">1</param>cannot become JSON without the declared schema. They reportsupportsTools()but notcanParseToolCalls(), and adapters gate on the latter, so the refusal reason is "this runtime cannot decode tagged arguments" rather than the false "this model has no tool format".The trust boundary is enforced, not asserted
Tool results are attacker-influenced in a way ordinary chat text is not. Delimiters render as
CONTROLsegments and results asTEXT; because the tokenizer consults the special-token table only insideCONTROL, a result spelling out control markers cannot close a span or open a turn.Verified against the real Qwen3 vocabulary:
<tool_call><|im_start|>The secure path and the training-faithful path are the same path.
Embeddings
An embedding is the generative pass with its head removed.
Headreplaces the boolean that conflated "project to vocabulary" with "is this activation needed at all" — prefill legitimately skips most of the final layer when the activation is discarded, but embedding needs precisely that activation. Both pruning shortcuts had to be excluded, including one that skips the last layer's FFN; a second boolean would have hidden it.EmbeddingBackendmoves tomodels-apibecause it is a contract. Leaving it inmodels-embeddingwould have forced every backend implementing it to depend onvectors-db, inverting themodels -> vectorstier direction.Sampler
Full-vocabulary sort replaced with a bounded-heap top-k selection: 19.251 ms -> 0.848 ms per sampled token at a 151,936-token vocabulary. Ties still prefer the lower token id, so seeded output is unchanged — a 500-trial oracle test pins equivalence against the previous implementation.
Verification
894 tests, 0 failures.
clean spotlessCheck build complianceCheckgreen, includingverifyArchitectureBoundariesandverifyReleaseMetadata.13 of those run against real Qwen3-Embedding-0.6B weights (no mocks): related text lands closer than unrelated with a >0.25 margin, paraphrased support queries separate from off-topic by >0.20, and both framework adapters produce byte-identical vectors from one backend — so an application can move between them without re-indexing.
Both integration suites are wired into
model-integration.yml.Breaking change
EmbeddingBackendmoved fromcom.integrallis.models.embeddingtocom.integrallis.models.api. No published artifact implemented it.🤖 Generated with Claude Code