Skip to content

Conversation

kpavlov
Copy link
Collaborator

@kpavlov kpavlov commented Oct 3, 2025

Support JSON schema for function parameters in chat models

Introduces SchemaHelper for JSON Schema parsing/introspection. Expands OpenAI chat models: FunctionObject.parameters now JsonElement and adds ToolChoiceSerializer. Adds new request-spec APIs and matchers for tool/function parameter assertions using SchemaHelper. Updates tests for tools, tool calls, multimodal parts, and adds JUnit parallel execution property.

Copy link

codacy-production bot commented Oct 3, 2025

Coverage summary from Codacy

See diff coverage on Codacy

Coverage variation Diff coverage
Report missing for 6af08ae1 6.87%
Coverage variation details
Coverable lines Covered lines Coverage
Common ancestor commit (6af08ae) Report Missing Report Missing Report Missing
Head commit (d872c5a) 7315 5012 68.52%

Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch: <coverage of head commit> - <coverage of common ancestor commit>

Diff coverage details
Coverable lines Covered lines Diff coverage
Pull request (#413) 131 9 6.87%

Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified: <covered lines added or modified>/<coverable lines added or modified> * 100%

See your quality gate settings    Change summary preferences

Footnotes

  1. Codacy didn't receive coverage data for the commit, or there was an error processing the received data. Check your integration for errors and validate that your coverage setup is correct.

@kpavlov kpavlov marked this pull request as ready for review October 3, 2025 16:13
Copy link
Contributor

coderabbitai bot commented Oct 3, 2025

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Support arbitrary JSON for tool/function parameters.
    • Added request-spec helpers to validate tools, parameters, types, and required fields.
    • Enhanced tool choice serialization (auto/none/function) for broader compatibility.
    • Introduced schema utilities for parsing and inspecting parameter schemas.
  • Tests

    • Expanded coverage for tools, tool calls, content parts (text, image_url, audio), and reasoning_effort; tightened assertions and renamed one test for clarity.
  • Chores

    • Enabled concurrent JUnit execution for faster test runs.
    • Adjusted JSON pretty-printing in tests to reduce noise.

Walkthrough

Adds a SchemaHelper singleton for JSON Schema parsing/introspection; changes FunctionObject.parameters to JsonElement; adds ToolChoiceSerializer; introduces request-spec APIs and matchers for tool/function parameter assertions that use SchemaHelper; expands tests for tools/multimodal inputs and enables concurrent JUnit execution.

Changes

Cohort / File(s) Summary
JSON Schema utilities
ai-mocks-core/src/commonMain/kotlin/me/kpavlov/aimocks/core/json/schema/SchemaHelper.kt
New public SchemaHelper object with a private Json instance and helpers: parseSchema, hasProperty, getPropertyType, isPropertyRequired, hasAllRequiredProperties, getProperty, getPropertyDescription.
OpenAI model: Function parameters & tool choice serializer
ai-mocks-openai/src/commonMain/kotlin/me/kpavlov/aimocks/openai/Model.kt, ai-mocks-openai/src/commonMain/kotlin/me/kpavlov/aimocks/openai/model/chat/ChatCompletionModels.kt
FunctionObject.parameters type changed from Map<String, String>?JsonElement?. Added ToolChoiceSerializer and annotated ToolChoice with @Serializable(with = ToolChoiceSerializer::class).
Request specification matchers API
ai-mocks-openai/src/commonMain/kotlin/me/kpavlov/aimocks/openai/completions/OpenaiChatCompletionRequestSpecification.kt
Added methods that enqueue tool-related matchers into requestBody: hasToolWithFunction, toolHasParameter (2 overloads), toolParameterHasType, toolRequiresParameters.
OpenAI completions matchers
ai-mocks-openai/src/commonMain/kotlin/me/kpavlov/aimocks/openai/completions/OpenaiCompletionsMatchers.kt
Added matchers to assert tool/function presence and parameter properties (existence, description, type, required), using SchemaHelper to introspect parameter schemas.
Tests: chat models and parsing
ai-mocks-openai/src/commonTest/kotlin/me/kpavlov/aimocks/openai/model/chat/ChatCompletionModelsTest.kt
Renamed a test and expanded coverage to validate single/multiple tools, tool choice, tool calls, multimodal parts (image_url, input_audio), reasoning_effort, and JSON-based parameter assertions.
JUnit config
ai-mocks-openai/src/jvmTest/resources/junit-platform.properties
Added junit.jupiter.execution.parallel.mode.default=concurrent.
Test formatting tweak
ai-mocks-openai/src/commonTest/kotlin/me/kpavlov/aimocks/openai/model/embeddings/EmbeddingModelsTest.kt
Changed test Json config to prettyPrint=false (affects formatting only).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Spec as RequestSpec
  participant Req as RequestBody
  participant M as OpenaiCompletionsMatchers
  participant SH as SchemaHelper

  Spec->>Req: toolHasParameter(fn, param)
  Spec->>Req: addMatcher(M.toolHasParameter(fn,param))

  Note over Req,M: During verification
  Req->>M: execute matcher(request)
  M->>M: locate tool by function name
  alt tool has parameters (JsonElement)
    M->>SH: parseSchema(parametersJson)
    SH-->>M: SchemaDefinition?
    M->>SH: getProperty / getPropertyType / isPropertyRequired
    SH-->>M: property info
  else parameters absent or non-schema
    M-->>M: treat as missing/inapplicable
  end
  M-->>Req: match result (pass/fail)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

tests, enhancement

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.46% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title succinctly and accurately conveys the primary change of adding JSON schema support for function parameters in chat models by referencing both JSON schema and function parameters in the context of chat models.
Description Check ✅ Passed The description clearly outlines the introduction of SchemaHelper for JSON Schema parsing, the change of FunctionObject.parameters to JsonElement, the addition of ToolChoiceSerializer, new request-spec APIs and matchers, and updates to tests and configuration. It directly corresponds to the changes in the pull request and is not off-topic. Therefore the description is sufficiently related to the changeset.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch openai-functions

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@kpavlov kpavlov added the refactoring Making things better label Oct 3, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6af08ae and 5f8b418.

📒 Files selected for processing (7)
  • ai-mocks-core/src/commonMain/kotlin/me/kpavlov/aimocks/core/json/schema/SchemaHelper.kt (1 hunks)
  • ai-mocks-openai/src/commonMain/kotlin/me/kpavlov/aimocks/openai/Model.kt (2 hunks)
  • ai-mocks-openai/src/commonMain/kotlin/me/kpavlov/aimocks/openai/completions/OpenaiChatCompletionRequestSpecification.kt (1 hunks)
  • ai-mocks-openai/src/commonMain/kotlin/me/kpavlov/aimocks/openai/completions/OpenaiCompletionsMatchers.kt (2 hunks)
  • ai-mocks-openai/src/commonMain/kotlin/me/kpavlov/aimocks/openai/model/chat/ChatCompletionModels.kt (4 hunks)
  • ai-mocks-openai/src/commonTest/kotlin/me/kpavlov/aimocks/openai/model/chat/ChatCompletionModelsTest.kt (5 hunks)
  • ai-mocks-openai/src/jvmTest/resources/junit-platform.properties (1 hunks)
🧰 Additional context used
🪛 detekt (1.23.8)
ai-mocks-core/src/commonMain/kotlin/me/kpavlov/aimocks/core/json/schema/SchemaHelper.kt

[warning] 27-27: The caught exception is too generic. Prefer catching specific exceptions to the case that is currently handled.

(detekt.exceptions.TooGenericExceptionCaught)


[warning] 27-27: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Codacy Static Code Analysis
  • GitHub Check: build

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5f8b418 and be0f4c8.

📒 Files selected for processing (8)
  • ai-mocks-core/src/commonMain/kotlin/me/kpavlov/aimocks/core/json/schema/SchemaHelper.kt (1 hunks)
  • ai-mocks-openai/src/commonMain/kotlin/me/kpavlov/aimocks/openai/Model.kt (2 hunks)
  • ai-mocks-openai/src/commonMain/kotlin/me/kpavlov/aimocks/openai/completions/OpenaiChatCompletionRequestSpecification.kt (1 hunks)
  • ai-mocks-openai/src/commonMain/kotlin/me/kpavlov/aimocks/openai/completions/OpenaiCompletionsMatchers.kt (2 hunks)
  • ai-mocks-openai/src/commonMain/kotlin/me/kpavlov/aimocks/openai/model/chat/ChatCompletionModels.kt (4 hunks)
  • ai-mocks-openai/src/commonTest/kotlin/me/kpavlov/aimocks/openai/model/chat/ChatCompletionModelsTest.kt (5 hunks)
  • ai-mocks-openai/src/commonTest/kotlin/me/kpavlov/aimocks/openai/model/embeddings/EmbeddingModelsTest.kt (1 hunks)
  • ai-mocks-openai/src/jvmTest/resources/junit-platform.properties (1 hunks)
🧰 Additional context used
🪛 detekt (1.23.8)
ai-mocks-openai/src/commonMain/kotlin/me/kpavlov/aimocks/openai/completions/OpenaiCompletionsMatchers.kt

[warning] 187-190: In most cases using a spread operator causes a full copy of the array to be created before calling a method. This may result in a performance penalty.

(detekt.performance.SpreadOperator)

🪛 GitHub Check: Codacy Static Code Analysis
ai-mocks-openai/src/commonMain/kotlin/me/kpavlov/aimocks/openai/model/chat/ChatCompletionModels.kt

[warning] 409-409: ai-mocks-openai/src/commonMain/kotlin/me/kpavlov/aimocks/openai/model/chat/ChatCompletionModels.kt#L409
Method deserialize has a cyclomatic complexity of 9 (limit is 8)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Codacy Static Code Analysis
  • GitHub Check: build
🔇 Additional comments (10)
ai-mocks-openai/src/jvmTest/resources/junit-platform.properties (1)

4-4: Confirm test readiness for method-level concurrency.

Switching junit.jupiter.execution.parallel.mode.default to concurrent means sibling test methods inside the same class now execute in parallel by default. Please re-run the suite (ideally multiple times) and ensure no tests depend on ordered execution or shared mutable state—especially those using @TestInstance(PER_CLASS), class-level fixtures, or static mocks. Update or guard any fragile tests before landing this change.

ai-mocks-openai/src/commonMain/kotlin/me/kpavlov/aimocks/openai/model/chat/ChatCompletionModels.kt (1)

425-476: Serializer fix looks solid

Thanks for handling both string and object payloads while re-emitting the required "type":"function" discriminator—this resolves the earlier decode/encode failure for spec-compliant tool choices.

ai-mocks-core/src/commonMain/kotlin/me/kpavlov/aimocks/core/json/schema/SchemaHelper.kt (3)

24-31: Past review comment has been addressed.

The exception handling now correctly catches only SerializationException, allowing other failures (like coroutine cancellations) to surface. The required import was also added.


82-85: Function implementation is correct.

The use of all provides early termination when a property is not required, making this efficient for the validation use case.


106-115: LGTM!

The null-safe property access pattern is consistent with other helper functions and correctly handles missing properties and unsupported variants.

ai-mocks-openai/src/commonMain/kotlin/me/kpavlov/aimocks/openai/completions/OpenaiCompletionsMatchers.kt (5)

56-69: LGTM!

The matcher correctly checks for tool existence by function name with proper null safety.


78-98: LGTM!

The matcher correctly chains nullable operations and uses SchemaHelper for property introspection. The implementation handles both missing tools and missing parameters correctly.


108-133: LGTM!

The description matching logic correctly uses SchemaHelper and handles missing descriptions appropriately.


143-168: LGTM!

The type checking logic correctly delegates to SchemaHelper and handles missing or null types appropriately.


177-218: LGTM with minor performance note.

The matcher correctly validates required parameters using SchemaHelper. The spread operator at line 189 creates an array copy, but in the context of request validation (not a hot path), the ergonomic vararg API is more valuable than micro-optimization.

Introduces SchemaHelper for JSON Schema parsing/introspection. Expands OpenAI chat models: FunctionObject.parameters now JsonElement and adds ToolChoiceSerializer. Adds new request-spec APIs and matchers for tool/function parameter assertions using SchemaHelper. Updates tests for tools, tool calls, multimodal parts, and adds JUnit parallel execution property.
@kpavlov kpavlov merged commit 6d8efbf into main Oct 3, 2025
6 of 8 checks passed
@kpavlov kpavlov deleted the openai-functions branch October 3, 2025 16:49
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ai-mocks-openai/src/commonMain/kotlin/me/kpavlov/aimocks/openai/Model.kt (1)

237-245: Avoid duplicated FunctionObject definitions across modules

This class also exists in model/chat. Consider centralizing to one definition to prevent drift.

♻️ Duplicate comments (1)
ai-mocks-openai/src/commonMain/kotlin/me/kpavlov/aimocks/openai/model/chat/ChatCompletionModels.kt (1)

370-385: ToolChoice now uses a custom serializer — resolves earlier decoding/encoding issues

Annotating the sealed class with ToolChoiceSerializer is the right fix; ensures proper string and object forms.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between be0f4c8 and d872c5a.

📒 Files selected for processing (8)
  • ai-mocks-core/src/commonMain/kotlin/me/kpavlov/aimocks/core/json/schema/SchemaHelper.kt (1 hunks)
  • ai-mocks-openai/src/commonMain/kotlin/me/kpavlov/aimocks/openai/Model.kt (2 hunks)
  • ai-mocks-openai/src/commonMain/kotlin/me/kpavlov/aimocks/openai/completions/OpenaiChatCompletionRequestSpecification.kt (1 hunks)
  • ai-mocks-openai/src/commonMain/kotlin/me/kpavlov/aimocks/openai/completions/OpenaiCompletionsMatchers.kt (2 hunks)
  • ai-mocks-openai/src/commonMain/kotlin/me/kpavlov/aimocks/openai/model/chat/ChatCompletionModels.kt (4 hunks)
  • ai-mocks-openai/src/commonTest/kotlin/me/kpavlov/aimocks/openai/model/chat/ChatCompletionModelsTest.kt (5 hunks)
  • ai-mocks-openai/src/commonTest/kotlin/me/kpavlov/aimocks/openai/model/embeddings/EmbeddingModelsTest.kt (1 hunks)
  • ai-mocks-openai/src/jvmTest/resources/junit-platform.properties (1 hunks)
🧰 Additional context used
🪛 GitHub Check: Codacy Static Code Analysis
ai-mocks-openai/src/commonMain/kotlin/me/kpavlov/aimocks/openai/model/chat/ChatCompletionModels.kt

[warning] 409-409: ai-mocks-openai/src/commonMain/kotlin/me/kpavlov/aimocks/openai/model/chat/ChatCompletionModels.kt#L409
Method deserialize has a cyclomatic complexity of 9 (limit is 8)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Codacy Static Code Analysis
  • GitHub Check: build
🔇 Additional comments (28)
ai-mocks-openai/src/commonTest/kotlin/me/kpavlov/aimocks/openai/model/embeddings/EmbeddingModelsTest.kt (1)

13-13: LGTM: prettyPrint disabled

This reduces formatting overhead; decoding tests remain unaffected.

ai-mocks-openai/src/commonTest/kotlin/me/kpavlov/aimocks/openai/model/chat/ChatCompletionModelsTest.kt (9)

12-14: LGTM: imports for JsonElement assertions

jsonObject/jsonPrimitive imports are appropriate for JsonElement-based parameters.


75-139: LGTM: single tool request with JSON Schema parameters

Covers object-shaped parameters and validates tool list; matches new API surface.


141-197: LGTM: multiple tools scenario

Validates presence of two functions and key request fields.


563-571: LGTM: Tool.parameters assertions migrated to JsonElement

Access via jsonObject/jsonPrimitive is correct after parameters → JsonElement change.


577-633: LGTM: image URL multimodal message test

Asserts typed parts; aligns with MessageContent.Parts and ImageUrlObject.


634-691: LGTM: tool_choice "auto" path covered

Confirms ToolChoiceSerializer handles string variant and tools array shape.


693-762: LGTM: assistant tool calls in ChatResponse

Checks function name and raw JSON arguments string as per API behavior.


764-813: LGTM: audio input part

Covers input_audio structure and fields.


815-839: LGTM: reasoning_effort field

Validates new request field deserialization.

ai-mocks-openai/src/commonMain/kotlin/me/kpavlov/aimocks/openai/Model.kt (1)

241-244: FunctionObject.parameters → JsonElement: good change

Allows full JSON Schema. Please ensure call sites migrated from Map access to JsonObject navigation.

ai-mocks-openai/src/commonMain/kotlin/me/kpavlov/aimocks/openai/model/chat/ChatCompletionModels.kt (2)

20-27: LGTM: required JSON imports added

Needed for JsonElement parameters and ToolChoiceSerializer.


305-315: FunctionObject.parameters → JsonElement

Correct to accept arbitrary JSON Schema. Verify downstream matchers and usages handle non-object shapes (arrays/enums).

ai-mocks-openai/src/commonMain/kotlin/me/kpavlov/aimocks/openai/completions/OpenaiCompletionsMatchers.kt (6)

5-5: LGTM!

The import is correctly added to support the new schema introspection functionality in the matchers below.


50-69: LGTM!

The matcher correctly validates the presence of a tool with the specified function name using null-safe operations.


78-98: LGTM!

The matcher correctly uses SchemaHelper to parse the schema and verify property existence with proper null-safety.


108-133: LGTM!

The matcher correctly validates both parameter existence and its description using exact string matching.


143-168: LGTM!

The matcher correctly handles type validation, including multi-type schemas, by checking if the expected type is contained in the list returned by getPropertyType.


177-218: LGTM!

The matcher correctly validates that all specified parameters are marked as required in the schema, with clear error messages.

ai-mocks-core/src/commonMain/kotlin/me/kpavlov/aimocks/core/json/schema/SchemaHelper.kt (9)

1-6: LGTM!

Package and imports are correctly organized, including the SerializationException import that addresses the previous review feedback.


12-16: LGTM!

The singleton design with a private, robustly configured Json instance is appropriate for schema parsing utilities.


24-31: LGTM! Previous feedback addressed.

The function now correctly catches only SerializationException, allowing other exceptions (including cancellations) to propagate while gracefully handling schema parse failures.


40-43: LGTM!

The property existence check is straightforward and correct.


52-61: LGTM! Previous feedback addressed.

The function now returns List<String>? to properly handle multi-type schemas, addressing the earlier limitation.


70-73: LGTM!

The required property check is straightforward and correct.


82-85: LGTM!

The implementation correctly validates that all specified properties are marked as required using idiomatic Kotlin.


94-97: LGTM!

The property retrieval is straightforward and provides access to the full property definition.


106-115: LGTM!

The description retrieval correctly handles different property types and returns null for non-value properties.

Comment on lines +42 to +108

/**
* Adds a matcher to verify that the request includes a tool with the specified function name.
*
* @param functionName The name of the function to match
*/
public fun hasToolWithFunction(functionName: String) {
requestBody.add(OpenaiCompletionsMatchers.hasToolWithFunction(functionName))
}

/**
* Adds a matcher to verify that a tool's function has a parameter with the specified name.
*
* @param functionName The name of the function
* @param parameterName The name of the parameter to check
*/
public fun toolHasParameter(
functionName: String,
parameterName: String,
) {
requestBody.add(OpenaiCompletionsMatchers.toolHasParameter(functionName, parameterName))
}

/**
* Adds a matcher to verify that a tool's function has a parameter with the specified name and description.
*
* @param functionName The name of the function
* @param parameterName The name of the parameter to check
* @param description The expected description of the parameter
*/
public fun toolHasParameter(
functionName: String,
parameterName: String,
description: String,
) {
requestBody.add(OpenaiCompletionsMatchers.toolHasParameter(functionName, parameterName, description))
}

/**
* Adds a matcher to verify that a tool's function parameter has the specified type.
*
* @param functionName The name of the function
* @param parameterName The name of the parameter
* @param expectedType The expected type (e.g., "string", "integer", "number", "boolean", "object", "array")
*/
public fun toolParameterHasType(
functionName: String,
parameterName: String,
expectedType: String,
) {
requestBody.add(
OpenaiCompletionsMatchers.toolParameterHasType(functionName, parameterName, expectedType),
)
}

/**
* Adds a matcher to verify that a tool's function requires specific parameters.
*
* @param functionName The name of the function
* @param requiredParams The parameter names that should be required
*/
public fun toolRequiresParameters(
functionName: String,
vararg requiredParams: String,
) {
requestBody.add(OpenaiCompletionsMatchers.toolRequiresParameters(functionName, *requiredParams))
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider returning this for fluent API consistency

seed(...) is chainable; these new methods return Unit. Returning the spec improves ergonomics without behavior change.

-    public fun hasToolWithFunction(functionName: String) {
-        requestBody.add(OpenaiCompletionsMatchers.hasToolWithFunction(functionName))
-    }
+    public fun hasToolWithFunction(functionName: String): OpenaiChatCompletionRequestSpecification =
+        apply { requestBody.add(OpenaiCompletionsMatchers.hasToolWithFunction(functionName)) }

-    public fun toolHasParameter(
+    public fun toolHasParameter(
         functionName: String,
         parameterName: String,
-    ) {
-        requestBody.add(OpenaiCompletionsMatchers.toolHasParameter(functionName, parameterName))
-    }
+    ): OpenaiChatCompletionRequestSpecification =
+        apply { requestBody.add(OpenaiCompletionsMatchers.toolHasParameter(functionName, parameterName)) }

-    public fun toolHasParameter(
+    public fun toolHasParameter(
         functionName: String,
         parameterName: String,
         description: String,
-    ) {
-        requestBody.add(OpenaiCompletionsMatchers.toolHasParameter(functionName, parameterName, description))
-    }
+    ): OpenaiChatCompletionRequestSpecification =
+        apply {
+            requestBody.add(
+                OpenaiCompletionsMatchers.toolHasParameter(functionName, parameterName, description),
+            )
+        }

-    public fun toolParameterHasType(
+    public fun toolParameterHasType(
         functionName: String,
         parameterName: String,
         expectedType: String,
-    ) {
-        requestBody.add(
-            OpenaiCompletionsMatchers.toolParameterHasType(functionName, parameterName, expectedType),
-        )
-    }
+    ): OpenaiChatCompletionRequestSpecification =
+        apply {
+            requestBody.add(
+                OpenaiCompletionsMatchers.toolParameterHasType(functionName, parameterName, expectedType),
+            )
+        }

-    public fun toolRequiresParameters(
+    public fun toolRequiresParameters(
         functionName: String,
         vararg requiredParams: String,
-    ) {
-        requestBody.add(OpenaiCompletionsMatchers.toolRequiresParameters(functionName, *requiredParams))
-    }
+    ): OpenaiChatCompletionRequestSpecification =
+        apply { requestBody.add(OpenaiCompletionsMatchers.toolRequiresParameters(functionName, *requiredParams)) }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Adds a matcher to verify that the request includes a tool with the specified function name.
*
* @param functionName The name of the function to match
*/
public fun hasToolWithFunction(functionName: String) {
requestBody.add(OpenaiCompletionsMatchers.hasToolWithFunction(functionName))
}
/**
* Adds a matcher to verify that a tool's function has a parameter with the specified name.
*
* @param functionName The name of the function
* @param parameterName The name of the parameter to check
*/
public fun toolHasParameter(
functionName: String,
parameterName: String,
) {
requestBody.add(OpenaiCompletionsMatchers.toolHasParameter(functionName, parameterName))
}
/**
* Adds a matcher to verify that a tool's function has a parameter with the specified name and description.
*
* @param functionName The name of the function
* @param parameterName The name of the parameter to check
* @param description The expected description of the parameter
*/
public fun toolHasParameter(
functionName: String,
parameterName: String,
description: String,
) {
requestBody.add(OpenaiCompletionsMatchers.toolHasParameter(functionName, parameterName, description))
}
/**
* Adds a matcher to verify that a tool's function parameter has the specified type.
*
* @param functionName The name of the function
* @param parameterName The name of the parameter
* @param expectedType The expected type (e.g., "string", "integer", "number", "boolean", "object", "array")
*/
public fun toolParameterHasType(
functionName: String,
parameterName: String,
expectedType: String,
) {
requestBody.add(
OpenaiCompletionsMatchers.toolParameterHasType(functionName, parameterName, expectedType),
)
}
/**
* Adds a matcher to verify that a tool's function requires specific parameters.
*
* @param functionName The name of the function
* @param requiredParams The parameter names that should be required
*/
public fun toolRequiresParameters(
functionName: String,
vararg requiredParams: String,
) {
requestBody.add(OpenaiCompletionsMatchers.toolRequiresParameters(functionName, *requiredParams))
}
/**
* Adds a matcher to verify that the request includes a tool with the specified function name.
*
* @param functionName The name of the function to match
*/
public fun hasToolWithFunction(functionName: String): OpenaiChatCompletionRequestSpecification =
apply { requestBody.add(OpenaiCompletionsMatchers.hasToolWithFunction(functionName)) }
/**
* Adds a matcher to verify that a tool's function has a parameter with the specified name.
*
* @param functionName The name of the function
* @param parameterName The name of the parameter to check
*/
public fun toolHasParameter(
functionName: String,
parameterName: String,
): OpenaiChatCompletionRequestSpecification =
apply { requestBody.add(OpenaiCompletionsMatchers.toolHasParameter(functionName, parameterName)) }
/**
* Adds a matcher to verify that a tool's function has a parameter with the specified name and description.
*
* @param functionName The name of the function
* @param parameterName The name of the parameter to check
* @param description The expected description of the parameter
*/
public fun toolHasParameter(
functionName: String,
parameterName: String,
description: String,
): OpenaiChatCompletionRequestSpecification =
apply {
requestBody.add(
OpenaiCompletionsMatchers.toolHasParameter(functionName, parameterName, description),
)
}
/**
* Adds a matcher to verify that a tool's function parameter has the specified type.
*
* @param functionName The name of the function
* @param parameterName The name of the parameter
* @param expectedType The expected type (e.g., "string", "integer", "number", "boolean", "object", "array")
*/
public fun toolParameterHasType(
functionName: String,
parameterName: String,
expectedType: String,
): OpenaiChatCompletionRequestSpecification =
apply {
requestBody.add(
OpenaiCompletionsMatchers.toolParameterHasType(functionName, parameterName, expectedType),
)
}
/**
* Adds a matcher to verify that a tool's function requires specific parameters.
*
* @param functionName The name of the function
* @param requiredParams The parameter names that should be required
*/
public fun toolRequiresParameters(
functionName: String,
vararg requiredParams: String,
): OpenaiChatCompletionRequestSpecification =
apply { requestBody.add(OpenaiCompletionsMatchers.toolRequiresParameters(functionName, *requiredParams)) }
🤖 Prompt for AI Agents
In
ai-mocks-openai/src/commonMain/kotlin/me/kpavlov/aimocks/openai/completions/OpenaiChatCompletionRequestSpecification.kt
around lines 42 to 108, the new helper methods (hasToolWithFunction, both
overloads of toolHasParameter, toolParameterHasType, and toolRequiresParameters)
currently return Unit which breaks fluent/chaining ergonomics; change each
method signature to return OpenaiChatCompletionRequestSpecification and add a
final "return this" after adding the matcher so callers can chain calls
(preserve existing matcher-add logic and vararg handling).

Comment on lines +397 to +479
/**
* Custom serializer for [ToolChoice] that handles both string and object formats.
*
* According to the OpenAI specification, tool_choice can be:
* - A simple string ("auto" or "none")
* - An object with type "function" and a function field
*
* This serializer automatically converts between these formats.
*/
public class ToolChoiceSerializer : KSerializer<ToolChoice> {
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("ToolChoice")

override fun deserialize(decoder: Decoder): ToolChoice {
val jsonDecoder =
decoder as? JsonDecoder
?: throw SerializationException("This serializer can only be used with JSON")

return when (val element = jsonDecoder.decodeJsonElement()) {
is JsonPrimitive -> {
when (element.contentOrNull) {
"auto" -> ToolChoice.Auto
"none" -> ToolChoice.None
else -> throw SerializationException(
"Unknown tool choice: ${element.contentOrNull}",
)
}
}

is JsonObject -> {
val type = element["type"]?.jsonPrimitive?.contentOrNull ?: "function"
if (type != "function") {
throw SerializationException("Unknown tool choice type: $type")
}
val functionElement =
element["function"]
?: throw SerializationException("tool_choice.function is required")
val function =
jsonDecoder.json.decodeFromJsonElement(
ToolChoiceFunction.serializer(),
functionElement,
)
ToolChoice.Function(function)
}

else -> throw SerializationException("Unsupported tool choice payload: $element")
}
}

override fun serialize(
encoder: Encoder,
value: ToolChoice,
) {
val jsonEncoder =
encoder as? JsonEncoder
?: throw SerializationException("This serializer can only be used with JSON")

when (value) {
is ToolChoice.Auto -> {
jsonEncoder.encodeJsonElement(JsonPrimitive("auto"))
}

is ToolChoice.None -> {
jsonEncoder.encodeJsonElement(JsonPrimitive("none"))
}

is ToolChoice.Function -> {
val functionElement =
jsonEncoder.json.encodeToJsonElement(
ToolChoiceFunction.serializer(),
value.function,
)
jsonEncoder.encodeJsonElement(
JsonObject(
mapOf(
"type" to JsonPrimitive("function"),
"function" to functionElement,
),
),
)
}
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

ToolChoiceSerializer correctly handles string/object forms; minor complexity nit

  • Validates "type":"function", requires "function", encodes discriminator — spec-compliant. Nice.
  • Cyclomatic complexity slightly above threshold; extract tiny helpers to appease static checks.
 class ToolChoiceSerializer : KSerializer<ToolChoice> {
-    override fun deserialize(decoder: Decoder): ToolChoice {
+    override fun deserialize(decoder: Decoder): ToolChoice {
         val jsonDecoder = decoder as? JsonDecoder
             ?: throw SerializationException("This serializer can only be used with JSON")
-        return when (val element = jsonDecoder.decodeJsonElement()) {
-            is JsonPrimitive -> {
-                when (element.contentOrNull) {
-                    "auto" -> ToolChoice.Auto
-                    "none" -> ToolChoice.None
-                    else -> throw SerializationException("Unknown tool choice: ${element.contentOrNull}")
-                }
-            }
-            is JsonObject -> {
-                val type = element["type"]?.jsonPrimitive?.contentOrNull ?: "function"
-                if (type != "function") {
-                    throw SerializationException("Unknown tool choice type: $type")
-                }
-                val functionElement = element["function"]
-                    ?: throw SerializationException("tool_choice.function is required")
-                val function = jsonDecoder.json.decodeFromJsonElement(
-                    ToolChoiceFunction.serializer(), functionElement)
-                ToolChoice.Function(function)
-            }
-            else -> throw SerializationException("Unsupported tool choice payload: $element")
-        }
+        val element = jsonDecoder.decodeJsonElement()
+        return when (element) {
+            is JsonPrimitive -> decodePrimitive(element)
+            is JsonObject -> decodeObject(jsonDecoder, element)
+            else -> throw SerializationException("Unsupported tool choice payload: $element")
+        }
     }
+
+    private fun decodePrimitive(p: JsonPrimitive): ToolChoice =
+        when (p.contentOrNull) {
+            "auto" -> ToolChoice.Auto
+            "none" -> ToolChoice.None
+            else -> throw SerializationException("Unknown tool choice: ${p.contentOrNull}")
+        }
+
+    private fun decodeObject(dec: JsonDecoder, obj: JsonObject): ToolChoice {
+        val type = obj["type"]?.jsonPrimitive?.contentOrNull ?: "function"
+        if (type != "function") throw SerializationException("Unknown tool choice type: $type")
+        val fnEl = obj["function"] ?: throw SerializationException("tool_choice.function is required")
+        val fn = dec.json.decodeFromJsonElement(ToolChoiceFunction.serializer(), fnEl)
+        return ToolChoice.Function(fn)
+    }
🧰 Tools
🪛 GitHub Check: Codacy Static Code Analysis

[warning] 409-409: ai-mocks-openai/src/commonMain/kotlin/me/kpavlov/aimocks/openai/model/chat/ChatCompletionModels.kt#L409
Method deserialize has a cyclomatic complexity of 9 (limit is 8)

🤖 Prompt for AI Agents
ai-mocks-openai/src/commonMain/kotlin/me/kpavlov/aimocks/openai/model/chat/ChatCompletionModels.kt
around lines 397 to 479: the ToolChoiceSerializer is functionally correct but
exceeds cyclomatic complexity; refactor by extracting small private helpers to
reduce branching in deserialize/serialize. Concretely, keep the
JsonDecoder/JsonEncoder casts and top-level deserialize/serialize methods, but
move the JsonPrimitive handling into a private fun parseFromPrimitive(element:
JsonPrimitive): ToolChoice and the JsonObject handling into private fun
parseFromObject(obj: JsonObject, json: Json): ToolChoice (validate type, require
function, decode function), and for serialization move primitive and object
branches into private helpers like encodePrimitive(value: ToolChoice,
jsonEncoder: JsonEncoder) and encodeFunctionObject(function, jsonEncoder).
Ensure all thrown SerializationException messages and semantics remain unchanged
and wire the helpers from the original methods so behavior is identical while
reducing cyclomatic complexity.

## https://docs.junit.org/5.3.0-M1/user-guide/index.html#writing-tests-parallel-execution
junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.config.strategy=dynamic
junit.jupiter.execution.parallel.mode.default=concurrent
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Parallel mode default set to concurrent — verify test/thread safety

Good for speed. Please ensure:

  • No shared mutable state in tests/mocks (singletons like SchemaHelper, Json instances).
  • Unique ports/resources if any HTTP servers are used.
  • Deterministic seeds where order-sensitive.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

refactoring Making things better

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant