Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
7 changes: 7 additions & 0 deletions connectors/categories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,13 @@ export const LEAF_CATEGORIES = defineLeafCategories([
description:
"Generate production 3D meshes from text prompts and photos.",
},
{
id: "text-generation",
displayName: "Text Generation",
description:
"Generate text with large language models, including chat, " +
"reasoning, tool calling, structured output, and more.",
},
{
id: "image-generation",
displayName: "Image Generation",
Expand Down
7 changes: 6 additions & 1 deletion connectors/ids.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -629,7 +629,12 @@
"suzanne#v1/models/download",
"suzanne#v1/uploads",
"tinyfish#fetch",
"tinyfish#search"
"tinyfish#search",
"venice#augment/scrape",
"venice#augment/search",
"venice#chat/completions",
"venice#embeddings",
"venice#image/generate"
],
"resources": [
"saperly/phone-number"
Expand Down
93 changes: 93 additions & 0 deletions connectors/venice/endpoints/augment-scrape/endpoint.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { assertEquals, assertRejects } from "@std/assert";
import { fromFileUrl } from "@std/path";
import {
estimateEndpoint,
liveSkip,
loadFixture,
runEndpoint,
testSealedUnit,
} from "@shared/testing";

const ID = "venice#augment/scrape";
const chains = fromFileUrl(new URL("../../fixtures/", import.meta.url));

Deno.test(`${ID} happy (recorded): flat $0.01, markdown content`, async () => {
const unit = await testSealedUnit(ID);
const result = await runEndpoint({
unit,
input: { body: { url: "https://example.com" } },
mode: "replay",
fixture: await loadFixture(`${chains}scrape-ok.json`),
});
assertEquals(result.httpStatus, 200);
assertEquals(result.isProviderError, false);
assertEquals(result.usage, {
credits: { default: 0.01 },
evidence: { CALL: 1 },
});
const output = result.output as Record<string, unknown>;
assertEquals(output.format, "markdown");
assertEquals(typeof output.content, "string");
});

Deno.test(`${ID} blocked site (recorded 400): zero usage, digested error`, async () => {
const unit = await testSealedUnit(ID);
const result = await runEndpoint({
unit,
input: { body: { url: "https://x.com/venice_ai" } },
mode: "replay",
fixture: await loadFixture(`${chains}scrape-blocked.json`),
});
assertEquals(result.httpStatus, 400);
assertEquals(result.isProviderError, true);
assertEquals(result.usage, { credits: {}, evidence: {} });
const output = result.output as Record<string, unknown>;
assertEquals(
(output.message as string).includes("blocks automated access"),
true,
);
});

Deno.test(`${ID} schema gate: a non-URL is rejected before the wire`, async () => {
const unit = await testSealedUnit(ID);
await assertRejects(
() =>
runEndpoint({
unit,
input: { body: { url: "not a url" } },
mode: "replay",
fixture: {
name: "unused",
description: "never reached",
calls: [],
},
}),
Error,
"INVALID_INPUT",
);
// Passing near-twin: a different valid https URL clears the gate.
const accepted = await estimateEndpoint(unit, {
body: { url: "https://example.org/valid-page" },
});
assertEquals(accepted.evidence, { CALL: 1 });
});

Deno.test({
name: `${ID} live (gated on VENICE_API_KEY)`,
ignore: liveSkip("venice"),
fn: async () => {
const unit = await testSealedUnit(ID);
const result = await runEndpoint({
unit,
input: { body: { url: "https://example.com" } },
mode: "live",
});
assertEquals(
result.isProviderError,
false,
JSON.stringify(result.output),
);
const output = result.output as Record<string, unknown>;
assertEquals(output.format, "markdown");
},
});
41 changes: 41 additions & 0 deletions connectors/venice/endpoints/augment-scrape/endpoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { defineEndpoint, UsageModelKind } from "@shared/core";
import { zAugmentScrapeBody } from "./schema/inputs.ts";

/**
* Venice `POST /augment/scrape` — fetch one URL and return it as Markdown.
* Flat $0.01 per successful call (Venice price sheet: `scrape-augmentation`
* $10 per 1,000, verified 2026-10-08); rejected and failed URLs are not
* charged. No meter on the response — the derived fold is the bill.
*/
export default defineEndpoint({
meta: {
displayName: "Venice Web Scrape",
summary: "Fetch a public web page as clean Markdown.",
description: "Fetch one public URL and return its main content as " +
'Markdown ({url, content, format: "markdown"}). Useful after ' +
"`venice#augment/search` to read a result in full. Sites that " +
"block automated access — notably X/Twitter and Reddit — are " +
"rejected immediately (and not charged); private, internal and " +
"localhost addresses are refused.",
docsUrl: "https://docs.venice.ai/api-reference/endpoint/augment/scrape",
categories: ["web-scraping"],
notes: [
"Flat $0.01 per page.",
"Venice rate-limits this endpoint at 20 requests per minute " +
"per account.",
"X/Twitter and Reddit URLs are refused with a 4xx before any " +
"fetch.",
],
},
request: { method: "POST", path: "/augment/scrape" },
input: { schema: { body: zAugmentScrapeBody } },
usage: {
model: {
kind: UsageModelKind.PER_CALL,
consumes: { credit: "default", amount: 0.01 },
label: "page",
description: "one successfully scraped page",
},
},
timeouts: { requestMs: 60_000, runMs: 60_000 },
});
12 changes: 12 additions & 0 deletions connectors/venice/endpoints/augment-scrape/schema/inputs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { z } from "zod";

/**
* `POST /augment/scrape` request body — mirrors Venice's
* `WebScrapeRequest`. Optionality only (D25).
*/
export const zAugmentScrapeBody = z.object({
url: z.string().url().describe(
"Public http(s) URL to fetch. Private/internal addresses are " +
"rejected.",
),
});
121 changes: 121 additions & 0 deletions connectors/venice/endpoints/augment-search/endpoint.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { assertEquals, assertRejects } from "@std/assert";
import { fromFileUrl } from "@std/path";
import {
estimateEndpoint,
liveSkip,
loadFixture,
runEndpoint,
testSealedUnit,
} from "@shared/testing";

const ID = "venice#augment/search";
const chains = fromFileUrl(new URL("../../fixtures/", import.meta.url));

Deno.test(`${ID} happy (recorded): flat $0.01, structured results`, async () => {
const unit = await testSealedUnit(ID);
const result = await runEndpoint({
unit,
input: { body: { query: "deno 2.9 release notes", limit: 3 } },
mode: "replay",
fixture: await loadFixture(`${chains}search-ok.json`),
});
assertEquals(result.httpStatus, 200);
assertEquals(result.isProviderError, false);
assertEquals(result.usage, {
credits: { default: 0.01 },
evidence: { CALL: 1 },
});
const output = result.output as Record<string, unknown>;
const results = output.results as Record<string, unknown>[];
assertEquals(typeof results[0].url, "string");
assertEquals(typeof results[0].title, "string");
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Deno.test(`${ID} provider error (recorded 401): zero usage, digested error`, async () => {
const unit = await testSealedUnit(ID);
const result = await runEndpoint({
unit,
input: { body: { query: "venice ai", limit: 3 } },
mode: "replay",
fixture: await loadFixture(`${chains}unauthorized.json`),
});
assertEquals(result.httpStatus, 401);
assertEquals(result.isProviderError, true);
assertEquals(result.usage, { credits: {}, evidence: {} });
const output = result.output as Record<string, unknown>;
assertEquals(output.message, "Authentication failed");
});

Deno.test(`${ID} provider error (429): no usage`, async () => {
const unit = await testSealedUnit(ID);
const result = await runEndpoint({
unit,
input: { body: { query: "deno 2.9 release notes", limit: 3 } },
mode: "replay",
fixture: {
name: "synthetic-augment-search-rate-limited",
description: "A Venice 429 must not bill search usage.",
calls: [{
req: { method: "POST", url: unit.doc.request.url },
res: { status: 429, body: { error: "rate limit exceeded" } },
}],
},
});
assertEquals(result.httpStatus, 429);
assertEquals(result.isProviderError, true);
assertEquals(result.usage, { credits: {}, evidence: {} });
const output = result.output as Record<string, unknown>;
assertEquals(output.message, "rate limit exceeded");
});

Deno.test(`${ID} schema gate: limit over 20 and unknown backends are rejected`, async () => {
const unit = await testSealedUnit(ID);
const fixture = await loadFixture(`${chains}search-ok.json`);
await assertRejects(
() =>
runEndpoint({
unit,
input: { body: { query: "q", limit: 21 } },
mode: "replay",
fixture,
}),
Error,
"INVALID_INPUT",
);
await assertRejects(
() =>
runEndpoint({
unit,
input: { body: { query: "q", search_provider: "bing" } },
mode: "replay",
fixture,
}),
Error,
"INVALID_INPUT",
);
// Passing near-twin: limit 20 is allowed.
const accepted = await estimateEndpoint(unit, {
body: { query: "q", limit: 20 },
});
assertEquals(accepted.evidence, { CALL: 1 });
});

Deno.test({
name: `${ID} live (gated on VENICE_API_KEY)`,
ignore: liveSkip("venice"),
fn: async () => {
const unit = await testSealedUnit(ID);
const result = await runEndpoint({
unit,
input: { body: { query: "venice ai", limit: 2 } },
mode: "live",
});
assertEquals(
result.isProviderError,
false,
JSON.stringify(result.output),
);
const output = result.output as Record<string, unknown>;
assertEquals(Array.isArray(output.results), true);
},
});
52 changes: 52 additions & 0 deletions connectors/venice/endpoints/augment-search/endpoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { defineEndpoint, UsageModelKind } from "@shared/core";
import { zAugmentSearchBody } from "./schema/inputs.ts";

/**
* Venice `POST /augment/search` — live web search returning structured
* results. Flat $0.01 per successful call (Venice price sheet:
* `search-augmentation` $10 per 1,000, verified 2026-10-08); errors are
* not charged. No meter on the response — the derived fold is the bill.
*/
export default defineEndpoint({
meta: {
displayName: "Venice Web Search",
summary: "Private live web search — Brave (zero data retention) or " +
"anonymized Google.",
description: "Search the live web and get structured results " +
"({title, url, content, date}) — up to 20 per call. Two " +
"privacy-preserving backends: `brave` (default) runs with Zero " +
"Data Retention, so the query is never stored or logged; " +
"`google` proxies the query through Venice so your identity is " +
"never attached to it. `content` is a page excerpt, often " +
"several paragraphs. Pipe a result URL to " +
"`venice#augment/scrape` for the full page as Markdown, or use " +
"`venice#chat/completions` with web search enabled when you " +
"want a synthesized answer instead of raw results.",
docsUrl: "https://docs.venice.ai/api-reference/endpoint/augment/search",
categories: ["web-search"],
notes: [
"Flat $0.01 per call, whatever `limit` is.",
"Venice rate-limits this endpoint at 20 requests per minute " +
"per account.",
],
},
request: { method: "POST", path: "/augment/search" },
input: {
schema: {
body: zAugmentSearchBody.extend({
limit: zAugmentSearchBody.shape.limit.unwrap().default(10),
search_provider: zAugmentSearchBody.shape.search_provider
.unwrap().default("brave"),
}),
},
},
usage: {
model: {
kind: UsageModelKind.PER_CALL,
consumes: { credit: "default", amount: 0.01 },
label: "search",
description: "one successful web search",
},
},
timeouts: { requestMs: 60_000, runMs: 60_000 },
});
18 changes: 18 additions & 0 deletions connectors/venice/endpoints/augment-search/schema/inputs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { z } from "zod";

/**
* `POST /augment/search` request body — mirrors Venice's
* `WebSearchRequest` (https://docs.venice.ai). Optionality only (D25).
*/
export const zAugmentSearchBody = z.object({
query: z.string().min(1).max(400).describe("The search query."),
limit: z.number().int().min(1).max(20).optional().describe(
"Maximum results to return (vendor default 10, max 20).",
),
search_provider: z.enum(["brave", "google"]).optional().describe(
"`brave` (vendor default): Brave Search with Zero Data Retention " +
"— queries are never stored or logged. `google`: Google " +
"Search, anonymized — proxied so your identity is not " +
"attached to the query.",
),
});
Loading