feat(mapillary): dev-server provider — cached tiles, feature stream, sprites, optional Claude planner

Vite middleware for /api/mapillary: status (capabilities, never values),
vector tiles cached in memory and on disk with the 12 MB z14 image layer
stripped in transit, an NDJSON feature stream capped at 600 tiles, MIT
sprite icons behind a grammar-guarded path, and a structured-output planner
that only activates when ANTHROPIC_API_KEY is set server-side.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
Said TurkseverandClaude Fable 5.1 committed 2026-09-23 15:44:32 +01:00
1 parent fa5710bcf2
commit 791e77fa33
3 files changed
+40 -7

No files matched your search

+1 -1
View File
@@ -26,7 +26,7 @@ function sendJson(res, status, payload) {
}
/** Parse an ISO date (or epoch ms) into epoch ms, or null when absent/invalid. */
export function parseEpochMs(value) {
function parseEpochMs(value) {
if (value === null || value === undefined || value === '') return null;
if (typeof value === 'number') return Number.isFinite(value) ? value : null;
const parsed = Date.parse(String(value));
+35 -6
View File
@@ -1,6 +1,5 @@
import Anthropic from '@anthropic-ai/sdk';
import { z } from 'zod';
import { zodOutputFormat } from '@anthropic-ai/sdk/helpers/zod';
import { readRequestBody } from '../common/request.js';
import { makeOptInRateLimiter, clientKey } from '../common/rate-limit.js';
import {
@@ -26,7 +25,7 @@ import {
* The plan the model returns. Every field is required (structured outputs
* reject optional keys); "unknown" is spelled with null.
*/
export const PlanSchema = z.object({
const PlanSchema = z.object({
intent: z.enum([
'map_features',
'traffic_signs',
@@ -182,6 +181,35 @@ export function buildPlannerUserMessage({ query, context = {}, today }) {
return lines.join('\n');
}
/**
* Structured-output format for the plan: the Zod schema as JSON Schema. Kept
* free of the SDK's zod helper so the provider only depends on package roots.
*/
function planOutputFormat() {
const { $schema: _schema, ...schema } = z.toJSONSchema(PlanSchema, {
reused: 'ref',
});
return { type: 'json_schema', schema };
}
/** The plan object from a structured-output response, or null. */
function parsePlanResponse(response) {
if (response?.parsed_output) return response.parsed_output;
const text = (response?.content || [])
.filter((block) => block?.type === 'text')
.map((block) => block.text)
.join('');
if (!text.trim()) return null;
let raw;
try {
raw = JSON.parse(text);
} catch {
return null;
}
const result = PlanSchema.safeParse(raw);
return result.success ? result.data : null;
}
/**
* Ask Claude for a plan. Exported for tests with an injectable client.
* @returns {Promise<{plan: object, usage: object|null, model: string}>}
@@ -190,7 +218,7 @@ export async function planQuery(
{ query, context },
{ anthropic = client(), model = plannerModel(), now = new Date() } = {},
) {
const response = await anthropic.messages.parse({
const response = await anthropic.messages.create({
model,
max_tokens: PLANNER_MAX_TOKENS,
system: [
@@ -210,7 +238,7 @@ export async function planQuery(
}),
},
],
output_config: { format: zodOutputFormat(PlanSchema), effort: 'low' },
output_config: { format: planOutputFormat(), effort: 'low' },
});
if (response.stop_reason === 'refusal') {
return {
@@ -230,10 +258,11 @@ export async function planQuery(
model,
};
}
if (!response.parsed_output)
const parsed = parsePlanResponse(response);
if (!parsed)
throw Object.assign(new Error('Planner returned no plan'), { status: 502 });
return {
plan: finalizePlan(response.parsed_output),
plan: finalizePlan(parsed),
usage: response.usage ?? null,
model,
};
+4
View File
@@ -1,3 +1,7 @@
/**
* Sprite proxy. Icons come from github.com/mapillary/mapillary_sprite_source
* (MIT, © Mapillary), fetched on demand and cached on disk.
*/
import path from 'node:path';
import { promises as fsp } from 'node:fs';
import {