Skip to content
New site — Virtual Media Folders now has its own home. Visit vmf.soderlind.no →

Developer guide

How to call Jev from your own WordPress code — the PHP helper API, the JevClient class, the REST proxy, the bundled JavaScript client, and the plugin’s hooks.

Jev evaluates a state (the content) against a map of typed questions and returns one structured answer per question. There are three question types:

TypetypeExtra field criteriaAnswer fields
Noulnouloptional { true, false } descriptionsnoul (0–1)
Choicechoicerequired map option => description|nullchoice, probabilities, confidence
Scorescorerequired ordered array (≥ 2 levels)score, legend, probabilities, confidence

Ask as many questions as you like in a single request — each is evaluated in isolation against the same state.

Every value resolves from the settings page first, then a constant or environment variable. Define secrets in wp-config.php to keep them out of the database:

define( 'TYPESAFE_API_KEY', 'sk-...' );
define( 'TYPESAFE_MODEL', 'jev-latest' ); // optional
define( 'TYPESAFE_ENDPOINT', 'https://api.typesafe.ai/v1' ); // optional

Read the effective configuration at runtime:

use AiProviderForJev\Settings\SettingsManager;
$settings = SettingsManager::instance();
$settings->get_model(); // e.g. "jev-latest"
$settings->get_endpoint(); // e.g. "https://api.typesafe.ai/v1"
$settings->is_configured(); // true when an API key is available

The helper functions live in the AiProviderForJev namespace and are loaded on every request. Each returns the answer payload, or a WP_Error on failure.

use function AiProviderForJev\ask_noul;
use function AiProviderForJev\ask_choice;
use function AiProviderForJev\ask_score;
use function AiProviderForJev\evaluate;
ask_noul( string|array|object $state, string $instructions, ?array $criteria = null, ?string $model = null ): float|WP_Error
$state = 'Help! My payouts have been failing for 3 days.';
$probability = ask_noul( $state, 'Does this message express urgency?' );
// e.g. 0.92
if ( is_wp_error( $probability ) ) {
error_log( $probability->get_error_message() );
} elseif ( $probability > 0.8 ) {
// treat as urgent
}

Add criteria to define what “yes” and “no” mean:

$probability = ask_noul(
$state,
'Is this message urgent?',
[
'true' => 'Explicitly time-sensitive or blocking the customer',
'false' => 'No urgency expressed',
]
);
ask_choice( string|array|object $state, string $instructions, array $criteria, ?string $model = null ): array|WP_Error
$answer = ask_choice(
$state,
'Which team should handle this?',
[
'billing' => 'Payments, invoicing, refunds',
'technical' => 'Bugs, outages, integrations',
'sales' => 'Pricing, upgrades, new accounts',
]
);
// $answer = [
// 'choice' => 'billing',
// 'probabilities' => [ 'billing' => 0.84, 'technical' => 0.15, 'sales' => 0.01 ],
// 'confidence' => 0.59,
// ];
if ( ! is_wp_error( $answer ) && $answer['confidence'] < 0.5 ) {
// low confidence — route to a human
}

Pass null for an option that needs no description:

$answer = ask_choice( $state, 'Sentiment?', [
'positive' => null,
'neutral' => null,
'negative' => null,
] );
ask_score( string|array|object $state, string $instructions, array $criteria, ?string $model = null ): array|WP_Error
$answer = ask_score(
$state,
'How frustrated is the customer?',
[ 'Calm, just stating facts', 'Frustrated but civil', 'Very angry, strong language' ]
);
// $answer = [
// 'score' => 1.6,
// 'legend' => [ '0' => 'Calm...', '1' => 'Frustrated...', '2' => 'Very angry...' ],
// 'probabilities' => [ '0' => 0.05, '1' => 0.30, '2' => 0.65 ],
// 'confidence' => 0.78,
// ];
evaluate( string|array|object $state, array $questions, ?string $model = null ): array|WP_Error
$response = evaluate( $state, [
'is_urgent' => [
'type' => 'noul',
'instructions' => 'Does this convey urgency?',
],
'department' => [
'type' => 'choice',
'instructions' => 'Which team should handle this?',
'criteria' => [
'billing' => 'Payment or subscription issues',
'technical' => 'Bugs or integration problems',
'sales' => 'Pricing or account questions',
],
],
'frustration' => [
'type' => 'score',
'instructions' => 'How frustrated is the customer?',
'criteria' => [ 'Calm', 'Frustrated', 'Very angry' ],
],
] );
if ( is_wp_error( $response ) ) {
return;
}
$response['answers']['is_urgent']['noul']; // 0.999
$response['answers']['department']['choice']; // 'billing'
$response['answers']['frustration']['score']; // 1.035
$response['usage']; // [ 'input_tokens' => …, 'output_tokens' => … ]

state can be a string, an array, or an object — useful for chat logs or records:

$response = evaluate(
[
[ 'role' => 'customer', 'text' => 'My card was charged twice.' ],
[ 'role' => 'agent', 'text' => 'Let me check that for you.' ],
],
[ 'refund' => [ 'type' => 'noul', 'instructions' => 'Does the customer want a refund?' ] ]
);
ask_noul( $state, 'Urgent?', null, 'jev-1.13.0' ); // pin a version
evaluate( $state, $questions, 'jev-preview' );

For full control, use JevClient directly. It reads configuration from SettingsManager unless you inject your own.

use AiProviderForJev\Client\JevClient;
$client = new JevClient();
$response = $client->system_one(
$state,
[ 'q' => [ 'type' => 'noul', 'instructions' => 'Urgent?' ] ]
);
$models = $client->list_models(); // [ 'models' => [ [ 'name' => 'jev-latest', ... ] ] ]

On a non-2xx response the client returns a WP_Error with code jev_api_error; the HTTP status is available via the error data:

if ( is_wp_error( $response ) ) {
$status = $response->get_error_data()['status'] ?? 0; // 401, 422, 429, 529, …
}

The plugin registers an authenticated proxy that calls Jev with the site’s stored API key, so the key never reaches the browser.

MethodRouteBody
POST/wp-json/ai-provider-for-jev/v1/systemone{ state, questions, model? }
GET/wp-json/ai-provider-for-jev/v1/models

Access requires the manage_options capability by default — see Hooks and filters to change it. Requests must be authenticated (a logged-in user with a REST nonce, or an application password).

POST /wp-json/ai-provider-for-jev/v1/systemone
{
"state": "Help! My payouts have been failing for 3 days.",
"questions": {
"is_urgent": { "type": "noul", "instructions": "Does this convey urgency?" }
},
"model": "jev-latest"
}
{
"model": "jev-1.13.0",
"answers": {
"is_urgent": { "type": "noul", "noul": 0.92 }
},
"usage": { "input_tokens": 312, "output_tokens": 48 }
}

On failure the proxy mirrors the upstream status (401/422/429/529) and returns a WP_Error-shaped body: { "code": "jev_api_error", "message": "…", "data": { "status": 401 } }.

Terminal window
curl -X POST https://example.test/wp-json/ai-provider-for-jev/v1/systemone \
-u "admin:xxxx xxxx xxxx xxxx xxxx xxxx" \
-H "Content-Type: application/json" \
-d '{"state":"Help!","questions":{"q":{"type":"noul","instructions":"Urgent?"}}}'

apiFetch adds the REST nonce automatically inside wp-admin:

import apiFetch from '@wordpress/api-fetch';
const result = await apiFetch( {
path: '/ai-provider-for-jev/v1/systemone',
method: 'POST',
data: {
state: 'Help!',
questions: { is_urgent: { type: 'noul', instructions: 'Urgent?' } },
},
} );
console.log( result.answers.is_urgent.noul );

The plugin ships a dependency-free ES module at src/js/client.js that wraps the REST proxy.

import { systemOne, listModels } from '../src/js/client.js';
const result = await systemOne(
{
state: 'Help!',
questions: { is_urgent: { type: 'noul', instructions: 'Urgent?' } },
model: 'jev-latest', // optional
},
{ root: '/wp-json/ai-provider-for-jev/v1', nonce: window.wpApiSettings?.nonce }
);
const models = await listModels( { root: '/wp-json/ai-provider-for-jev/v1' } );

fetch, root, and nonce can be injected via the second argument (handy for tests) or read from a global window.aiProviderForJev object you localize. The module is not enqueued for you; register it from your own plugin/theme, for example:

add_action( 'wp_enqueue_scripts', function () {
wp_register_script_module(
'my-theme/jev',
get_stylesheet_directory_uri() . '/js/jev.js',
[],
'1.0.0'
);
wp_enqueue_script_module( 'my-theme/jev' );
// Expose the REST base and nonce to the module.
wp_add_inline_script(
'my-theme/jev',
'window.aiProviderForJev = ' . wp_json_encode( [
'root' => esc_url_raw( rest_url( 'ai-provider-for-jev/v1' ) ),
'nonce' => wp_create_nonce( 'wp_rest' ),
] ) . ';',
'before'
);
} );

Errors reject with an Error carrying .status and .data:

try {
await systemOne( { state: 's', questions: {} } );
} catch ( error ) {
console.error( error.message, error.status, error.data );
}

Filters the capability required to call the REST proxy. Default manage_options.

// Allow editors to use the proxy.
add_filter( 'ai_provider_jev_rest_capability', fn() => 'edit_posts' );
// Restrict by context — e.g. only allow it on the front end for a custom role.
add_filter( 'ai_provider_jev_rest_capability', function ( $capability ) {
return is_admin() ? $capability : 'use_jev_api';
} );

The three settings are plain options, so you can read or set them with core functions and hook into their standard filters.

PurposeOption nameConstant / env fallback
API keyai_provider_jev_api_keyTYPESAFE_API_KEY
Modelai_provider_jev_modelTYPESAFE_MODEL
API base URLai_provider_jev_endpointTYPESAFE_ENDPOINT
// Set the model at activation, or in a deploy script.
update_option( 'ai_provider_jev_model', 'jev-1.13.0' );
// Force the model via a core option filter (read-only override).
add_filter( 'option_ai_provider_jev_model', fn() => 'jev-preview' );

Note: reading ai_provider_jev_api_key with get_option() returns a masked value (bullets + last four characters). Prefer a constant/env var for the key, or read the real value through AiProviderForJev\Settings\SettingsManager::instance()->get_api_key().

A complete, copy-pasteable example lives at docs/examples/jev-comment-triage/. Jev Comment Triage auto-moderates new comments: it asks Jev for a spam probability (Noul) and a toxicity score (Score), then routes the comment and stores the scores as comment meta.

It demonstrates the patterns you’ll reuse in your own integrations:

  • Depend on the provider via the Requires Plugins: ai-provider-for-jev header, and guard at runtime with function_exists( 'AiProviderForJev\\evaluate' ) plus SettingsManager::instance()->is_configured().
  • Batch questions in a single evaluate() call.
  • Fail open — any WP_Error from the API leaves WordPress’s own decision untouched, so an outage never blocks commenting.
  • Act on the numbers with thresholds you control in code.

The core of it:

$scores = \AiProviderForJev\evaluate( $content, [
'is_spam' => [ 'type' => 'noul', 'instructions' => 'Is this comment spam?' ],
'toxicity' => [
'type' => 'score',
'instructions' => 'How toxic or abusive is this comment?',
'criteria' => [ 'Civil', 'Rude', 'Abusive or hateful' ],
],
] );
if ( is_wp_error( $scores ) ) {
return $approved; // fail open
}
if ( $scores['answers']['is_spam']['noul'] > 0.85 ) {
return 'spam';
}
if ( $scores['answers']['toxicity']['score'] >= 1.5 ) {
return '0'; // hold for moderation
}

To try it, copy the jev-comment-triage folder into wp-content/plugins/, activate AI Provider for Jev (configured with an API key), then activate Jev Comment Triage.

See Benchmark for its measured latency and production notes.

PHP tests use Pest with Brain Monkey; JavaScript tests use Vitest.

Terminal window
composer install
composer test # Pest
npm install
npm test # Vitest